From 7dc9d6dfa0859312f9bad2dcd590caa83982725b Mon Sep 17 00:00:00 2001 From: alikia2x Date: Tue, 10 Mar 2026 01:36:20 +0800 Subject: [PATCH 01/37] feat(auth): implement registration and identity endpoints (#3) Add endpoints for registration and fetching current user: POST /user to register a new user GET /me to fetch the current user (if Authorization header is provided) --- .gitignore | 1 + .tokeignore | 5 + LICENSE | 661 ++++++++++++++++++ README.md | 109 ++- bun.lock | 41 +- package.json | 25 +- .../migration.sql | 24 +- prisma/models/auth/session.prisma | 4 +- prisma/models/auth/user.prisma | 6 +- prisma/models/core/artist.prisma | 2 +- prisma/models/meta/history.prisma | 4 +- prisma/models/platform/file.prisma | 4 +- prisma/models/platform/post.prisma | 4 +- .../models/platform/reputationHistory.prisma | 6 +- src/containers.ts | 9 + src/index.ts | 65 +- src/lib/env.ts | 13 + src/lib/error/conflict.ts | 12 + src/lib/error/index.ts | 12 + src/lib/error/rateLimit.ts | 7 + src/lib/prisma.ts | 9 + src/onAfterHandle.ts | 4 +- src/repositories/SessionRepository.ts | 64 ++ src/repositories/UserRepository.ts | 54 ++ src/repositories/index.ts | 4 + .../interfaces/ISessionRepository.ts | 23 + .../interfaces/IUserRepository.ts | 16 + src/repositories/interfaces/index.ts | 6 + src/routes/auth/currentUser.ts | 48 ++ src/routes/auth/index.ts | 5 + src/routes/auth/signup.ts | 50 ++ src/schema.ts | 39 -- src/schemas/auth/currentUser.ts | 8 + src/schemas/auth/index.ts | 2 + src/schemas/auth/signup.ts | 19 + src/schemas/common/ErrorResponseSchema.ts | 6 + src/schemas/common/index.ts | 1 + src/services/AuthService.ts | 173 +++++ src/services/index.ts | 2 + src/services/interfaces/IAuthService.ts | 18 + src/services/interfaces/index.ts | 1 + src/startMessage.ts | 11 +- tests/e2e/auth.test.ts | 168 +++++ .../repositories/SessionRepository.test.ts | 92 +++ .../repositories/UserRepository.test.ts | 98 +++ tests/unit/lib/conflictError.test.ts | 21 + tests/unit/services/AuthService.test.ts | 166 +++++ tsconfig.json | 12 + 48 files changed, 2039 insertions(+), 95 deletions(-) create mode 100644 .tokeignore create mode 100644 LICENSE rename prisma/migrations/{20260226131523_init => 20260227101459_init}/migration.sql (97%) create mode 100644 src/containers.ts create mode 100644 src/lib/env.ts create mode 100644 src/lib/error/conflict.ts create mode 100644 src/lib/error/index.ts create mode 100644 src/lib/error/rateLimit.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/repositories/SessionRepository.ts create mode 100644 src/repositories/UserRepository.ts create mode 100644 src/repositories/index.ts create mode 100644 src/repositories/interfaces/ISessionRepository.ts create mode 100644 src/repositories/interfaces/IUserRepository.ts create mode 100644 src/repositories/interfaces/index.ts create mode 100644 src/routes/auth/currentUser.ts create mode 100644 src/routes/auth/index.ts create mode 100644 src/routes/auth/signup.ts delete mode 100644 src/schema.ts create mode 100644 src/schemas/auth/currentUser.ts create mode 100644 src/schemas/auth/index.ts create mode 100644 src/schemas/auth/signup.ts create mode 100644 src/schemas/common/ErrorResponseSchema.ts create mode 100644 src/schemas/common/index.ts create mode 100644 src/services/AuthService.ts create mode 100644 src/services/index.ts create mode 100644 src/services/interfaces/IAuthService.ts create mode 100644 src/services/interfaces/index.ts create mode 100644 tests/e2e/auth.test.ts create mode 100644 tests/integration/repositories/SessionRepository.test.ts create mode 100644 tests/integration/repositories/UserRepository.test.ts create mode 100644 tests/unit/lib/conflictError.test.ts create mode 100644 tests/unit/services/AuthService.test.ts diff --git a/.gitignore b/.gitignore index c05ccd7..9e09f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .env .env.development.local .env.test.local +.env.test .env.production.local .env.local diff --git a/.tokeignore b/.tokeignore new file mode 100644 index 0000000..ac5f108 --- /dev/null +++ b/.tokeignore @@ -0,0 +1,5 @@ +prisma/ +bruno/ +*.json +*.sql +*.md \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/README.md b/README.md index c9d2941..f1fc599 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,112 @@ -# cvsa-backend +# CVSA Backend -To install dependencies: +The backend service for the **Chinese Vocal Synthesizer Archive (CVSA)** – a unified platform for the Chinese vocal synthesizer community. +This service provides a RESTful API for the rest of the CVSA system and third parties to interact with. + +## Tech Stack + +| Layer | Technology | +| ----------- | --------------------------------------------------------------------------------------- | +| Runtime | [Bun](https://bun.sh) – fast JavaScript runtime, package manager, and test runner | +| Framework | [Elysia](https://elysiajs.com) – high‑performance, type‑friendly web framework | +| Database | PostgreSQL | +| ORM | [Prisma](https://www.prisma.io) – type‑safe database client with migrations | +| Testing | Bun test (unit, integration, E2E) | + +## Getting Started + +### Prerequisites + +- [Bun](https://bun.sh) >= 1.3.10 +- PostgreSQL >= 18 + +### Installation + +1. **Clone the repository** + + ```bash + git clone https://github.com/project-cvsa/cvsa-backend + cd cvsa-backend + ``` + +2. **Install dependencies** + + ```bash + bun install + ``` + +3. **Set up environment variables** + Copy the example file and edit it with your database credentials: + + ```bash + cp .env.example .env + ``` + + Required variables: + + ```ini + DATABASE_URL="postgresql://user:password@localhost:5432/cvsa" + ``` + +4. **Generate Prisma client and run migrations** + + ```bash + bun db-gen # generate Prisma Client + bun migrate:dev # apply migrations to your database + ``` + + (For production, use `bun migrate` with custom scripts.) + +5. **Start the development server** + + ```bash + bun dev + ``` + + The server will start at `http://localhost:16412` (configurable via `PORT`). + +## Testing + +We use Bun’s built‑in test runner. + +Run all tests: + +```bash +bun test +``` + +For a specific suite: ```bash -bun install +bun test:unit +bun test:integration +bun test:e2e ``` -To run: +To set up the test database (requires `DATABASE_URL` pointing to a test database): ```bash -bun run index.ts +bun test:db:setup # runs migrations and resets the test DB +``` + + +## Project Structure + ``` +src/ +├── containers.ts # DI container wiring +├── index.ts # App entry point +├── onAfterHandle.ts # Global response handler +├── startMessage.ts # Startup logging +├── lib/ # Shared utilities +├── repositories/ # Data access layer +├── services/ # Business logic +├── routes/ # Elysia route definitions +└── schemas/ # Zod validation schemas +tests/ # Unit, integration, E2E tests +prisma/ # Prisma schema and migrations +``` + +## License -This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +This project is licensed under the AGPLv3 License – see the [LICENSE](LICENSE) file for details. diff --git a/bun.lock b/bun.lock index 982a591..3f45aa5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,11 +5,18 @@ "": { "name": "@project-cvsa/backend", "dependencies": { + "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", "@prisma/client": "^7.4.1", + "@t3-oss/env-core": "^0.13.10", "@types/pg": "^8.16.0", + "argon2": "^0.44.0", "chalk": "^5.6.2", "elysia": "^1.4.26", + "elysia-ip": "^1.0.10", + "elysia-rate-limit": "^4.5.0", + "nanoid": "^5.1.6", + "zod": "^4.3.6", }, "devDependencies": { "@biomejs/biome": "^2.4.4", @@ -23,6 +30,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="], @@ -57,10 +66,16 @@ "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], + + "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.1", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.1", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-AH9XrqvSoBAaStn0Gm/sAnF97pDKz8uLpNmn51j1S9O9dhUva6LIxGdoDiiU9VXRIR89wAJXsvJSy+mK40m2xw=="], "@prisma/client": ["@prisma/client@7.4.1", "", { "dependencies": { "@prisma/client-runtime-utils": "7.4.1" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-pgIll2W1NVdof37xLeyySW+yfQ4rI+ERGCRwnO3BjVOx42GpYq6jhTyuALK8VKirvJJIvImgfGDA2qwhYVvMuA=="], @@ -97,6 +112,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -109,6 +126,8 @@ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], @@ -129,11 +148,13 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], @@ -149,6 +170,10 @@ "elysia": ["elysia@1.4.26", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-oGhEFYwt+4M/U9w34XTdBen2KOQIa/PXne40zUkTWRgJlVJNBhepxOdjWeY1U0CbYoEOyZaj1tc79wOsuCxMYw=="], + "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], + + "elysia-rate-limit": ["elysia-rate-limit@4.5.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-nsbl3WLvrGiG/SdTgevAsjCUJhY34Bgf+7bDOYrjTPZyS7Hd4MLuLc4MUr9TFsSJWPvKpzyrX2HW8IWjRbex1Q=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], @@ -199,14 +224,20 @@ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -313,16 +344,22 @@ "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1" } }, "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1" } }, "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg=="], "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], } } diff --git a/package.json b/package.json index eeba97f..8ed3a01 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,23 @@ "module": "index.ts", "type": "module", "version": "0.0.1", - "private": true, "scripts": { "format": "bunx --bun biome format --write", + "type": "bunx --bun tsc --noEmit", "lint": "bunx --bun biome lint", - "dev": "bun --watch src/index.ts" + "dev": "bun --hot src/index.ts", + "start": "bun src/index.ts", + "test": "bun test", + "test:coverage": "bun test --coverage", + "test:unit": "bun test tests/unit", + "test:e2e": "bun test tests/e2e", + "test:integration": "bun test tests/integration", + "db-gen": "bunx --bun prisma generate", + "migrate": "bunx --bun prisma migrate dev --create-only", + "migrate:dev": "bunx --bun prisma migrate dev", + "db-sync": "bunx --bun prisma db push", + "test:db:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", + "db:studio": "bunx --bun prisma studio" }, "devDependencies": { "@biomejs/biome": "^2.4.4", @@ -19,10 +31,17 @@ "typescript": "^5" }, "dependencies": { + "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", "@prisma/client": "^7.4.1", + "@t3-oss/env-core": "^0.13.10", "@types/pg": "^8.16.0", + "argon2": "^0.44.0", "chalk": "^5.6.2", - "elysia": "^1.4.26" + "elysia": "^1.4.26", + "elysia-ip": "^1.0.10", + "elysia-rate-limit": "^4.5.0", + "nanoid": "^5.1.6", + "zod": "^4.3.6" } } diff --git a/prisma/migrations/20260226131523_init/migration.sql b/prisma/migrations/20260227101459_init/migration.sql similarity index 97% rename from prisma/migrations/20260226131523_init/migration.sql rename to prisma/migrations/20260227101459_init/migration.sql index f2e4257..e86dee7 100644 --- a/prisma/migrations/20260226131523_init/migration.sql +++ b/prisma/migrations/20260227101459_init/migration.sql @@ -22,22 +22,22 @@ CREATE TYPE "meta"."change_type" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'RESTORE -- CreateTable CREATE TABLE "auth"."session" ( "id" TEXT NOT NULL, - "user_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, "ip_address" TEXT, "user_agent" TEXT, "secret_hash" TEXT NOT NULL, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "last_verified_at" TIMESTAMP(3) NOT NULL, + "last_verified_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "session_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "auth"."user" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "username" TEXT NOT NULL, - "display_name" TEXT NOT NULL, - "email" TEXT NOT NULL, + "display_name" TEXT, + "email" TEXT, "password" TEXT NOT NULL, "role_id" INTEGER, "reputation" INTEGER NOT NULL DEFAULT 0, @@ -73,7 +73,7 @@ CREATE TABLE "core"."artist" ( "localized_descriptions" JSONB, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, - "user_id" INTEGER, + "user_id" TEXT, "deleted_at" TIMESTAMP(3), CONSTRAINT "artist_pkey" PRIMARY KEY ("id") @@ -244,7 +244,7 @@ CREATE TABLE "core"."voicebank" ( -- CreateTable CREATE TABLE "meta"."history" ( "id" SERIAL NOT NULL, - "user_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, "table" TEXT NOT NULL, "object_id" INTEGER NOT NULL, "type" "meta"."change_type" NOT NULL, @@ -281,10 +281,10 @@ CREATE TABLE "meta"."role" ( -- CreateTable CREATE TABLE "platform"."file" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "original_name" TEXT, "path" TEXT, - "user_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "file_pkey" PRIMARY KEY ("id") @@ -292,9 +292,9 @@ CREATE TABLE "platform"."file" ( -- CreateTable CREATE TABLE "platform"."post" ( - "id" SERIAL NOT NULL, + "id" TEXT NOT NULL, "text" TEXT, - "user_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, "deleted_at" TIMESTAMP(3), @@ -305,7 +305,7 @@ CREATE TABLE "platform"."post" ( -- CreateTable CREATE TABLE "platform"."reputation_history" ( "id" SERIAL NOT NULL, - "user_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, "action_type" TEXT NOT NULL, "points" INTEGER NOT NULL, "source_type" INTEGER NOT NULL, diff --git a/prisma/models/auth/session.prisma b/prisma/models/auth/session.prisma index f999826..bca0256 100644 --- a/prisma/models/auth/session.prisma +++ b/prisma/models/auth/session.prisma @@ -1,12 +1,12 @@ model Session { id String @id user User @relation(fields: [userId], references: [id]) - userId Int @map("user_id") + userId String @map("user_id") ipAddress String? @map("ip_address") userAgent String? @map("user_agent") secretHash String @map("secret_hash") createdAt DateTime @default(now()) @map("created_at") - lastVerifiedAt DateTime @map("last_verified_at") + lastVerifiedAt DateTime @default(now()) @map("last_verified_at") @@map("session") @@schema("auth") diff --git a/prisma/models/auth/user.prisma b/prisma/models/auth/user.prisma index e316ebf..f3260cc 100644 --- a/prisma/models/auth/user.prisma +++ b/prisma/models/auth/user.prisma @@ -1,8 +1,8 @@ model User { - id Int @id @default(autoincrement()) + id String @id username String @unique - displayName String @map("display_name") - email String @unique + displayName String? @map("display_name") + email String? @unique password String changesMade History[] role Role? @relation(fields: [roleId], references: [id]) diff --git a/prisma/models/core/artist.prisma b/prisma/models/core/artist.prisma index cfbe2d2..0debcae 100644 --- a/prisma/models/core/artist.prisma +++ b/prisma/models/core/artist.prisma @@ -11,7 +11,7 @@ model Artist { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User? @relation(fields: [userId], references: [id]) - userId Int? @unique @map("user_id") + userId String? @unique @map("user_id") deletedAt DateTime? @map("deleted_at") @@map("artist") diff --git a/prisma/models/meta/history.prisma b/prisma/models/meta/history.prisma index 4c6839c..1631630 100644 --- a/prisma/models/meta/history.prisma +++ b/prisma/models/meta/history.prisma @@ -1,9 +1,9 @@ model History { id Int @id @default(autoincrement()) user User @relation(fields: [userId], references: [id]) - userId Int @map("user_id") + userId String @map("user_id") table String - objectId Int @map("object_id") + objectId Int @map("object_id") type ChangeType comment String? old Json? diff --git a/prisma/models/platform/file.prisma b/prisma/models/platform/file.prisma index 0fcea7e..6c5f86b 100644 --- a/prisma/models/platform/file.prisma +++ b/prisma/models/platform/file.prisma @@ -1,9 +1,9 @@ model File { - id Int @id @default(autoincrement()) + id String @id originalName String? @map("original_name") path String? user User @relation(fields: [userId], references: [id]) - userId Int @map("user_id") + userId String @map("user_id") createdAt DateTime @default(now()) @map("created_at") @@map("file") diff --git a/prisma/models/platform/post.prisma b/prisma/models/platform/post.prisma index 3ee4c3c..ed231f6 100644 --- a/prisma/models/platform/post.prisma +++ b/prisma/models/platform/post.prisma @@ -1,8 +1,8 @@ model Post { - id Int @id @default(autoincrement()) + id String @id text String? user User @relation(fields: [userId], references: [id]) - userId Int @map("user_id") + userId String @map("user_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") diff --git a/prisma/models/platform/reputationHistory.prisma b/prisma/models/platform/reputationHistory.prisma index 4b635bc..57df585 100644 --- a/prisma/models/platform/reputationHistory.prisma +++ b/prisma/models/platform/reputationHistory.prisma @@ -1,11 +1,11 @@ model ReputationHistory { id Int @id @default(autoincrement()) user User @relation(fields: [userId], references: [id]) - userId Int @map("user_id") + userId String @map("user_id") actionType String @map("action_type") points Int - sourceType Int @map("source_type") - sourceId Int @map("source_id") + sourceType Int @map("source_type") + sourceId Int @map("source_id") createdAt DateTime @default(now()) @map("created_at") @@map("reputation_history") diff --git a/src/containers.ts b/src/containers.ts new file mode 100644 index 0000000..47c5f32 --- /dev/null +++ b/src/containers.ts @@ -0,0 +1,9 @@ +import { prisma } from "@lib/prisma"; +import { createUserRepository } from "@repositories/UserRepository"; +import { createSessionRepository } from "@repositories/SessionRepository"; +import { createAuthService } from "@services/AuthService"; + +const userRepo = createUserRepository(prisma); +const sessionRepo = createSessionRepository(prisma); + +export const authService = createAuthService(userRepo, sessionRepo, prisma); diff --git a/src/index.ts b/src/index.ts index 1b64857..fb3a3f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,27 +2,64 @@ import { Elysia, type ErrorHandler } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; +import { authHandler } from "@routes/auth"; +import { AppError } from "@lib/error"; +import { ZodError } from "zod"; const [host, port] = getBindingInfo(); logStartup(host, port); -const errorHandler: ErrorHandler = ({ code, status, error }) => { - if (code === "NOT_FOUND") - return status(404, { - message: "The requested resource was not found.", - }); - if (code === "VALIDATION") return error.detail(error.message); - return error; +const errorHandler: ErrorHandler<{ + readonly AppError: AppError; +}> = ({ code, status, error }) => { + if (code === "NOT_FOUND") + return status(404, { + message: "The requested resource was not found.", + }); + if (code === "VALIDATION") { + const detail = error.detail(error.message); + if (typeof detail === "string") { + return status(422, { + code: "VALIDATION_ERROR", + message: detail, + }); + } + return status(422, { + code: "VALIDATION_ERROR", + message: detail.summary, + }); + } + if (error instanceof AppError) { + return status(error.statusCode, { + code: error.code, + message: error.message, + }); + } + if (error instanceof ZodError) { + return status(422, { + code: "VALIDATION_ERROR", + message: error.message, + }); + } + return status(500, { + code: "SERVER_ERROR", + message: "Internal server error", + }); }; -const app = new Elysia({ - serve: { - hostname: host, - }, +export const app = new Elysia({ + serve: { + hostname: host, + }, + prefix: "/v2", }) - .onError(errorHandler) - .use(onAfterHandler) - .listen(16412); + .error({ + AppError, + }) + .onError(errorHandler) + .use(authHandler) + .use(onAfterHandler) + .listen(16412); export const VERSION = pkg.version; diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..823f926 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,13 @@ +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + DATABASE_URL: z.url(), + NODE_ENV: z.enum(["development", "test", "production"]).optional(), + }, + + runtimeEnv: import.meta.env, + + emptyStringAsUndefined: true, +}); diff --git a/src/lib/error/conflict.ts b/src/lib/error/conflict.ts new file mode 100644 index 0000000..88dd464 --- /dev/null +++ b/src/lib/error/conflict.ts @@ -0,0 +1,12 @@ +import { AppError } from "@lib/error"; + +export class ConflictError extends AppError { + constructor( + field: string, + code: string = "APP_CONFLICT_ERROR", + message = "Record already exists", + statusCode: number = 409 + ) { + super(message, code, statusCode, { field }); + } +} diff --git a/src/lib/error/index.ts b/src/lib/error/index.ts new file mode 100644 index 0000000..8ce3151 --- /dev/null +++ b/src/lib/error/index.ts @@ -0,0 +1,12 @@ +export class AppError extends Error { + constructor( + public override message: string, + public code: string, + public statusCode: number = 500, + public meta?: Record + ) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/lib/error/rateLimit.ts b/src/lib/error/rateLimit.ts new file mode 100644 index 0000000..ebb1401 --- /dev/null +++ b/src/lib/error/rateLimit.ts @@ -0,0 +1,7 @@ +import { AppError } from "."; + +export class RateLimitError extends AppError { + constructor(message = "Request rate limit exceeded") { + super(message, "RATE_LIMIT_ERROR", 429); + } +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..676b8c6 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from "@prisma/generated/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { env } from "@lib/env"; + +const adapter = new PrismaPg({ + connectionString: env.DATABASE_URL, +}); + +export const prisma = new PrismaClient({ adapter }); diff --git a/src/onAfterHandle.ts b/src/onAfterHandle.ts index a5ca123..681d8bc 100644 --- a/src/onAfterHandle.ts +++ b/src/onAfterHandle.ts @@ -18,7 +18,7 @@ export const onAfterHandler = new Elysia().onAfterHandle( if (responseValue instanceof ElysiaFile || responseValue instanceof Response) { return; } - const realResponse = responseValue as Record; + const realResponse = responseValue as { code?: number; response?: unknown }; if (realResponse.code) { const text = isBrowser ? JSON.stringify(realResponse.response, null, 2) @@ -27,7 +27,7 @@ export const onAfterHandler = new Elysia().onAfterHandle( headers: { "Content-Type": "application/json; charset=utf-8", }, - status: realResponse.code as any, + status: realResponse.code, }); } const text = isBrowser diff --git a/src/repositories/SessionRepository.ts b/src/repositories/SessionRepository.ts new file mode 100644 index 0000000..b497114 --- /dev/null +++ b/src/repositories/SessionRepository.ts @@ -0,0 +1,64 @@ +import { prisma } from "@lib/prisma"; +import type { ISessionRepository, CreateSessionData, SessionWithSecret } from "./interfaces"; +import type { Session, Prisma, PrismaClient } from "@prisma/generated/client"; +import crypto from "node:crypto"; + +export class SessionRepository implements ISessionRepository { + private prisma: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.prisma = prismaClient; + } + + async create( + data: CreateSessionData, + transaction?: Prisma.TransactionClient + ): Promise { + const sha256Hasher = new Bun.CryptoHasher("sha256"); + // 120 bits entropy + const id = crypto.randomBytes(15).toString("hex"); + const secret = crypto.randomBytes(15).toString("hex"); + sha256Hasher.update(secret); + const secretHash = sha256Hasher.digest("hex"); + + const db = transaction ?? this.prisma; + const session = await db.session.create({ + data: { + id, + userId: data.userId, + secretHash: secretHash, + userAgent: data.userAgent, + ipAddress: data.ipAddress, + }, + }); + + // Return session with the plain secret for token generation + return Object.assign(session, { secret }); + } + + async findById(id: string, transaction?: Prisma.TransactionClient): Promise { + const db = transaction ?? this.prisma; + return await db.session.findUnique({ + where: { id }, + }); + } + + async findByIdAndSecretHash( + id: string, + secretHash: string, + transaction?: Prisma.TransactionClient + ): Promise { + const db = transaction ?? this.prisma; + return await db.session.findFirst({ + where: { + id, + secretHash, + }, + }); + } +} + +// Factory function to create SessionRepository with optional PrismaClient +export function createSessionRepository(prismaClient?: PrismaClient): SessionRepository { + return new SessionRepository(prismaClient); +} diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts new file mode 100644 index 0000000..d700ef7 --- /dev/null +++ b/src/repositories/UserRepository.ts @@ -0,0 +1,54 @@ +import { prisma } from "@lib/prisma"; +import type { IUserRepository, CreateUserData } from "./interfaces"; +import type { User } from "@prisma/generated/client"; +import type { Prisma, PrismaClient } from "@prisma/generated/client"; + +export class UserRepository implements IUserRepository { + private prisma: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.prisma = prismaClient; + } + + async create(data: CreateUserData, transaction?: Prisma.TransactionClient): Promise { + const db = transaction ?? this.prisma; + return await db.user.create({ + data: { + id: data.id, + username: data.username, + password: data.password, + displayName: data.displayName, + email: data.email, + }, + }); + } + + async findById(id: string, transaction?: Prisma.TransactionClient): Promise { + const db = transaction ?? this.prisma; + return await db.user.findUnique({ + where: { id }, + }); + } + + async findByUsername( + username: string, + transaction?: Prisma.TransactionClient + ): Promise { + const db = transaction ?? this.prisma; + return await db.user.findUnique({ + where: { username }, + }); + } + + async findByEmail(email: string, transaction?: Prisma.TransactionClient): Promise { + const db = transaction ?? this.prisma; + return await db.user.findUnique({ + where: { email }, + }); + } +} + +// Factory function to create UserRepository with optional PrismaClient +export function createUserRepository(prismaClient?: PrismaClient): UserRepository { + return new UserRepository(prismaClient); +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts new file mode 100644 index 0000000..d5b81ad --- /dev/null +++ b/src/repositories/index.ts @@ -0,0 +1,4 @@ +export type { IUserRepository, CreateUserData } from "./interfaces/IUserRepository"; +export type { ISessionRepository, CreateSessionData } from "./interfaces/ISessionRepository"; +export { UserRepository, createUserRepository } from "./UserRepository"; +export { SessionRepository, createSessionRepository } from "./SessionRepository"; diff --git a/src/repositories/interfaces/ISessionRepository.ts b/src/repositories/interfaces/ISessionRepository.ts new file mode 100644 index 0000000..7c84823 --- /dev/null +++ b/src/repositories/interfaces/ISessionRepository.ts @@ -0,0 +1,23 @@ +import type { Session, Prisma } from "@prisma/generated/client"; + +export interface CreateSessionData { + userId: string; + ipAddress?: string; + userAgent?: string; +} + +// Session with secret for token generation +export type SessionWithSecret = Session & { secret: string }; + +export interface ISessionRepository { + create( + data: CreateSessionData, + transaction?: Prisma.TransactionClient + ): Promise; + findById(id: string, transaction?: Prisma.TransactionClient): Promise; + findByIdAndSecretHash( + id: string, + secretHash: string, + transaction?: Prisma.TransactionClient + ): Promise; +} diff --git a/src/repositories/interfaces/IUserRepository.ts b/src/repositories/interfaces/IUserRepository.ts new file mode 100644 index 0000000..330090f --- /dev/null +++ b/src/repositories/interfaces/IUserRepository.ts @@ -0,0 +1,16 @@ +import type { User, Prisma } from "@prisma/generated/client"; + +export interface CreateUserData { + id: string; + username: string; + password: string; + displayName?: string | null; + email?: string | null; +} + +export interface IUserRepository { + create(data: CreateUserData, transaction?: Prisma.TransactionClient): Promise; + findById(id: string, transaction?: Prisma.TransactionClient): Promise; + findByUsername(username: string, transaction?: Prisma.TransactionClient): Promise; + findByEmail(email: string, transaction?: Prisma.TransactionClient): Promise; +} diff --git a/src/repositories/interfaces/index.ts b/src/repositories/interfaces/index.ts new file mode 100644 index 0000000..821c43b --- /dev/null +++ b/src/repositories/interfaces/index.ts @@ -0,0 +1,6 @@ +export type { IUserRepository, CreateUserData } from "./IUserRepository"; +export type { + ISessionRepository, + CreateSessionData, + SessionWithSecret, +} from "./ISessionRepository"; diff --git a/src/routes/auth/currentUser.ts b/src/routes/auth/currentUser.ts new file mode 100644 index 0000000..c8b5fd6 --- /dev/null +++ b/src/routes/auth/currentUser.ts @@ -0,0 +1,48 @@ +import { Elysia } from "elysia"; +import { GetCurrentUserResponseSchema } from "@schemas/auth"; +import { ErrorResponseSchema } from "@schemas/common"; +import z from "zod"; +import { authService } from "@/containers"; + +const DUMMY_TOKEN = "000000000000000000000000000000.000000000000000000000000000000"; +const DUMMY_BEARER_TOKEN = `Bearer ${DUMMY_TOKEN}`; + +const authSchema = z + .string() + // Length: Bearer (7) + hex(30) + dot(1) + hex(30) = 68 + .length(68) + .startsWith("Bearer ") + .transform((val) => val.slice(7)) + .refine((token) => /^[0-9a-fA-F]{30}\.[0-9a-fA-F]{30}$/.test(token)); + +export const getCurrentUserHandler = new Elysia().decorate("authService", authService).get( + "/me", + async ({ headers, status, authService }) => { + const auth = headers.authorization ?? DUMMY_BEARER_TOKEN; + const parsedToken = authSchema.safeParse(auth); + const token = parsedToken.success ? parsedToken.data : DUMMY_TOKEN; + const user = await authService.verifyToken(token); + + if (!parsedToken.success || !user) { + return status(401, { code: "UNAUTHORIZED", message: "Unauthorized" }); + } + return status(200, { + id: user.id, + username: user.username, + displayName: user.displayName, + email: user.email, + }); + }, + { + detail: { + summary: "Get current user", + description: "", + }, + response: { + 200: GetCurrentUserResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts new file mode 100644 index 0000000..77a1ee7 --- /dev/null +++ b/src/routes/auth/index.ts @@ -0,0 +1,5 @@ +import Elysia from "elysia"; +import { signupHandler } from "./signup"; +import { getCurrentUserHandler } from "./currentUser"; + +export const authHandler = new Elysia().use(signupHandler).use(getCurrentUserHandler); diff --git a/src/routes/auth/signup.ts b/src/routes/auth/signup.ts new file mode 100644 index 0000000..94aaad6 --- /dev/null +++ b/src/routes/auth/signup.ts @@ -0,0 +1,50 @@ +import { Elysia } from "elysia"; +import { ip } from "elysia-ip"; +import { rateLimit } from "elysia-rate-limit"; +import { SignupRequestSchema, SignupResponse200Schema } from "@schemas/auth"; +import { ErrorResponseSchema } from "@schemas/common"; +import { RateLimitError } from "@lib/error/rateLimit"; +import { authService } from "@/containers"; + +export const signupHandler = new Elysia() + .decorate("authService", authService) + .use(ip()) + .use( + rateLimit({ + scoping: "global", + max: 50, + duration: 5 * 60 * 1000, + generator: () => "", // global limit + errorResponse: new RateLimitError(), + }) + ) + .post( + "/user", + async ({ body, status, ip, headers, authService }) => { + const userAgent = headers["user-agent"]; + const { user, token } = await authService.register(body, ip, userAgent); + + return status(200, { + message: "Successfully registered", + data: { + id: user.id, + username: user.username, + displayName: user.displayName, + email: user.email, + token, + }, + }); + }, + { + body: SignupRequestSchema, + detail: { + summary: "User Registration", + description: "", + }, + response: { + 200: SignupResponse200Schema, + 400: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } + ); diff --git a/src/schema.ts b/src/schema.ts deleted file mode 100644 index 16e8eea..0000000 --- a/src/schema.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { t } from "elysia"; - -export const errorCodes = [ - "INVALID_QUERY_PARAMS", - "UNKNOWN_ERROR", - "INVALID_PAYLOAD", - "MALFORMED_SLOT", - "INVALID_HEADER", - "BODY_TOO_LARGE", - "UNAUTHORIZED", - "INVALID_CREDENTIALS", - "ENTITY_NOT_FOUND", - "SERVER_ERROR", - "RATE_LIMIT_EXCEEDED", - "ENTITY_EXISTS", - "THIRD_PARTY_ERROR", -]; - -function generateErrorCodeRegex(strings: string[]): string { - if (strings.length === 0) { - return "(?!)"; - } - - const escapedStrings = strings.map((str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); - - return `^(${escapedStrings.join("|")})$`; -} - -export const ErrorResponseSchema = t.Object({ - code: t.String({ pattern: generateErrorCodeRegex(errorCodes) }), - errors: t.Array(t.String()), - i18n: t.Optional( - t.Object({ - key: t.String(), - values: t.Optional(t.Record(t.String(), t.Union([t.String(), t.Number(), t.Date()]))), - }) - ), - message: t.String(), -}); diff --git a/src/schemas/auth/currentUser.ts b/src/schemas/auth/currentUser.ts new file mode 100644 index 0000000..7c9b994 --- /dev/null +++ b/src/schemas/auth/currentUser.ts @@ -0,0 +1,8 @@ +import z from "zod"; + +export const GetCurrentUserResponseSchema = z.object({ + id: z.string(), + username: z.string(), + displayName: z.string().optional().nullable(), + email: z.email().optional().nullable(), +}); diff --git a/src/schemas/auth/index.ts b/src/schemas/auth/index.ts new file mode 100644 index 0000000..ace014d --- /dev/null +++ b/src/schemas/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./signup"; +export * from "./currentUser"; diff --git a/src/schemas/auth/signup.ts b/src/schemas/auth/signup.ts new file mode 100644 index 0000000..6ab3cf0 --- /dev/null +++ b/src/schemas/auth/signup.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +export const SignupRequestSchema = z.object({ + username: z.string().min(1), + password: z.string().min(6), + displayName: z.string().max(100).optional().nullable(), + email: z.email().optional().nullable(), +}); + +export const SignupResponse200Schema = z.object({ + message: z.string(), + data: z.object({ + id: z.string(), + username: z.string(), + displayName: z.string().optional().nullable(), + email: z.email().optional().nullable(), + token: z.string(), + }), +}); diff --git a/src/schemas/common/ErrorResponseSchema.ts b/src/schemas/common/ErrorResponseSchema.ts new file mode 100644 index 0000000..0489581 --- /dev/null +++ b/src/schemas/common/ErrorResponseSchema.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +export const ErrorResponseSchema = z.object({ + code: z.string().optional(), + message: z.string().optional(), +}); diff --git a/src/schemas/common/index.ts b/src/schemas/common/index.ts new file mode 100644 index 0000000..6df971a --- /dev/null +++ b/src/schemas/common/index.ts @@ -0,0 +1 @@ +export * from "./ErrorResponseSchema"; diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..5db5cdd --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,173 @@ +import type { IAuthService, CreateUserData, AuthResult } from "./interfaces"; +import type { IUserRepository } from "@repositories/interfaces/IUserRepository"; +import type { ISessionRepository } from "@repositories/interfaces/ISessionRepository"; +import { prisma } from "@lib/prisma"; +import { AppError } from "@lib/error"; +import { ConflictError } from "@lib/error/conflict"; +import { Prisma, type User, type PrismaClient } from "@prisma/generated/client"; +import { customAlphabet } from "nanoid"; +import crypto from "node:crypto"; + +const MAX_RETRIES = 5; +const alphabet = "123456789ACDEFGHJKLMNPQRSTUVWXYZ"; +const nanoid = customAlphabet(alphabet, 7); +const PASSWORD_HASH_COST = 4; +const SESSION_SECRET_HASH_ALGORITHM = "sha256"; +const DUMMY_SESSION_SECRET = "0".repeat(30); +const DUMMY_PASSWORD_HASH = await Bun.password.hash(DUMMY_SESSION_SECRET, { + algorithm: "argon2id", + memoryCost: PASSWORD_HASH_COST, +}); + +function hashSessionSecret(secret: string): string { + const sha256Hasher = new Bun.CryptoHasher(SESSION_SECRET_HASH_ALGORITHM); + sha256Hasher.update(secret); + return sha256Hasher.digest("hex"); +} + +function fixedLengthEqual(left: string, right: string): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + + if (leftBuffer.length !== rightBuffer.length) { + return false; + } + + return crypto.timingSafeEqual(leftBuffer, rightBuffer); +} + +interface AuthServiceOptions { + passwordVerify?: typeof Bun.password.verify; +} + +export class AuthService implements IAuthService { + private userRepository: IUserRepository; + private sessionRepository: ISessionRepository; + private prisma: PrismaClient; + private passwordVerify: typeof Bun.password.verify; + + constructor( + userRepo: IUserRepository, + sessionRepo: ISessionRepository, + prismaClient: PrismaClient = prisma, + options: AuthServiceOptions = {} + ) { + this.userRepository = userRepo; + this.sessionRepository = sessionRepo; + this.prisma = prismaClient; + this.passwordVerify = options.passwordVerify ?? Bun.password.verify; + } + + async register( + data: CreateUserData, + ipAddress?: string, + userAgent?: string + ): Promise { + return await this.prisma.$transaction(async (tx) => { + const hashedPassword = await Bun.password.hash(data.password, { + algorithm: "argon2id", + memoryCost: PASSWORD_HASH_COST, + }); + + let attempts = 0; + let user: User | null = null; + + while (attempts < MAX_RETRIES) { + try { + user = await this.userRepository.create( + { + id: nanoid(), + username: data.username, + password: hashedPassword, + displayName: data.displayName, + email: data.email, + }, + tx + ); + break; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") { + // Normalize duplicate failures to execute the same expensive work + // before surfacing whether the username/email already exists. + await this.passwordVerify(data.password, DUMMY_PASSWORD_HASH); + + // TODO: Prisma driver adapter P2002 workaround + // remove after prisma/prisma#28281 is fixed + // meta.target missing -> using undocumented driverAdapterError shape + const adapterError = e.meta?.driverAdapterError as + | { cause?: { constraint?: { fields?: string[] } } } + | undefined; + const fields = adapterError?.cause?.constraint?.fields; + + if (fields?.includes("username")) { + throw new ConflictError( + "username", + "USERNAME_DUPLICATED", + "User name duplicated" + ); + } + if (fields?.includes("email")) { + throw new ConflictError( + "email", + "EMAIL_DUPLICATED", + "This email has been registered" + ); + } + if (fields?.includes("id")) { + attempts++; + continue; + } + } + + throw e; + } + } + + if (!user) { + throw new AppError("Failed to register", "ID_GENERATION_FAILED"); + } + + const session = await this.sessionRepository.create( + { + userId: user.id, + ipAddress, + userAgent, + }, + tx + ); + const token = `${session.id}.${session.secret}`; + return { + user, + token, + }; + }); + } + + async verifyToken(token: string): Promise { + const [id, secret, ...rest] = token.split("."); + const hasValidShape = Boolean(id && secret) && rest.length === 0; + const sessionId = id ?? DUMMY_SESSION_SECRET; + const sessionSecret = secret ?? DUMMY_SESSION_SECRET; + const secretHash = hashSessionSecret(sessionSecret); + + const session = await this.sessionRepository.findById(sessionId); + const storedSecretHash = session?.secretHash ?? hashSessionSecret(DUMMY_SESSION_SECRET); + const hashesMatch = fixedLengthEqual(secretHash, storedSecretHash); + + if (!hasValidShape || !session || !hashesMatch) { + return null; + } + + const user = await this.userRepository.findById(session.userId); + return user; + } +} + +// Factory function to create AuthService +export function createAuthService( + userRepo: IUserRepository, + sessionRepo: ISessionRepository, + prismaClient?: PrismaClient +): AuthService { + return new AuthService(userRepo, sessionRepo, prismaClient); +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..ce4b42b --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,2 @@ +export type { IAuthService, CreateUserData, AuthResult } from "./interfaces"; +export { AuthService, createAuthService } from "./AuthService"; diff --git a/src/services/interfaces/IAuthService.ts b/src/services/interfaces/IAuthService.ts new file mode 100644 index 0000000..21e8fbf --- /dev/null +++ b/src/services/interfaces/IAuthService.ts @@ -0,0 +1,18 @@ +import type { User } from "@prisma/generated/client"; + +export interface CreateUserData { + username: string; + password: string; + displayName?: string | null; + email?: string | null; +} + +export interface AuthResult { + user: User; + token: string; +} + +export interface IAuthService { + register(data: CreateUserData, ipAddress?: string, userAgent?: string): Promise; + verifyToken(token: string): Promise; +} diff --git a/src/services/interfaces/index.ts b/src/services/interfaces/index.ts new file mode 100644 index 0000000..8b1b437 --- /dev/null +++ b/src/services/interfaces/index.ts @@ -0,0 +1 @@ +export type { IAuthService, CreateUserData, AuthResult } from "./IAuthService"; diff --git a/src/startMessage.ts b/src/startMessage.ts index 6d9ec8e..1aa2370 100644 --- a/src/startMessage.ts +++ b/src/startMessage.ts @@ -1,10 +1,15 @@ +import { env } from "@lib/env"; import chalk from "chalk"; import os from "node:os"; function getLocalIpAddress(): string { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { - for (const iface of interfaces[name]!) { + const networkInterfaces = interfaces[name]; + if (!networkInterfaces) { + continue; + } + for (const iface of networkInterfaces) { if (iface.family === "IPv4" && !iface.internal) { return iface.address; } @@ -14,6 +19,8 @@ function getLocalIpAddress(): string { } export function logStartup(hostname: string, port: number) { + const isTest = env.NODE_ENV === "test"; + if (isTest) return; const localUrl = `http://localhost:${port}`; const networkIp = hostname === "0.0.0.0" ? getLocalIpAddress() : ""; const networkUrl = networkIp ? `http://${networkIp}:${port}` : ""; @@ -29,7 +36,7 @@ export function logStartup(hostname: string, port: number) { } export function getBindingInfo(): [string, number] { - const DEFAULT_PORT = 15412; + const DEFAULT_PORT = 16412; const NODE_ENV = process.env.NODE_ENV || "production"; const HOST = process.env.HOST ?? (NODE_ENV === "development" ? "0.0.0.0" : "127.0.0.1"); const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT; diff --git a/tests/e2e/auth.test.ts b/tests/e2e/auth.test.ts new file mode 100644 index 0000000..54b023a --- /dev/null +++ b/tests/e2e/auth.test.ts @@ -0,0 +1,168 @@ +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@lib/prisma"; + +const api = treaty(app); + +describe("Registration E2E Tests - POST /v2/user", () => { + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + test("should register a new user", async () => { + const payload = { + username: "new_user", + password: "password123", + email: "first@example.com", + }; + + const { data, status } = await api.v2.user.post(payload); + + expect(status).toBe(200); + expect(data).toMatchObject({ + message: expect.any(String), + data: { + id: expect.any(String), + username: payload.username, + displayName: null, + email: payload.email, + token: expect.any(String), + }, + }); + }); + + test("should fail when username already exists", async () => { + const payload = { + username: "duplicate_user", + password: "password123", + email: "first@example.com", + }; + + // Create the first user + await api.v2.user.post(payload); + + // Attempt to register with the same username + const { error, status } = await api.v2.user.post({ + ...payload, + email: "second@example.com", + }); + + // Should return 409 Conflict + expect(status).toBe(409); + expect(error?.value).toMatchObject({ + code: "USERNAME_DUPLICATED", + }); + }); + + test("should fail with invalid email format", async () => { + const { status, error } = await api.v2.user.post({ + username: "valid_user", + password: "password123", + email: "invalid-email-format", // Invalid email + }); + + // Should be caught by Zod validation in SignupRequestSchema + expect(status).toBe(422); + // @ts-expect-error tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) + expect(error?.value?.code).toBe("VALIDATION_ERROR"); + }); +}); + +describe("Profile E2E Tests - GET /v2/me", () => { + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + test("should return user profile with valid token", async () => { + // 1. Register to get a token + const signup = await api.v2.user.post({ + username: "profile_user", + password: "password123", + }); + + const token = signup.data?.data.token; + expect(token).toBeDefined(); + + // 2. Fetch profile using the token in Authorization header + const { data, status } = await api.v2.me.get({ + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(200); + expect(data).toMatchObject({ + username: "profile_user", + }); + expect(data?.id).toBeString(); + }); + + test("should return 401 for malformed token", async () => { + const { status, error } = await api.v2.me.get({ + headers: { + authorization: "Bearer invalid.token.format", + }, + }); + + // Malformed token should be seen as unauthorized + expect(status).toBe(401); + // @ts-expect-error – tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) + expect(error?.value.code).toBe("UNAUTHORIZED"); + }); + + test("should return 401 for non-existent session", async () => { + // Manually craft a token that looks valid but doesn't exist in DB + // Format: hex(30).hex(30) + const fakeToken = "0123456789abcdef0123456789abcd.0123456789abcdef0123456789abcd"; + + const { status } = await api.v2.me.get({ + headers: { + authorization: `Bearer ${fakeToken}`, + }, + }); + + expect(status).toBe(401); + }); + + test("should return 401 when Authorization header is missing", async () => { + const { status } = await api.v2.me.get(); + + expect(status).toBe(401); + }); + + test("should reject a validly-shaped token with the wrong secret", async () => { + const signup = await api.v2.user.post({ + username: "wrong_secret_user", + password: "password123", + }); + + const token = signup.data?.data.token; + expect(token).toBeDefined(); + if (!token) { + throw new Error("Expected signup to return a token"); + } + + const [sessionId] = token.split("."); + const wrongSecretToken = `${sessionId}.000000000000000000000000000000`; + + const { status, error } = await api.v2.me.get({ + headers: { + authorization: `Bearer ${wrongSecretToken}`, + }, + }); + + expect(status).toBe(401); + // @ts-expect-error – tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) + expect(error?.value.code).toBe("UNAUTHORIZED"); + }); +}); diff --git a/tests/integration/repositories/SessionRepository.test.ts b/tests/integration/repositories/SessionRepository.test.ts new file mode 100644 index 0000000..810d7bb --- /dev/null +++ b/tests/integration/repositories/SessionRepository.test.ts @@ -0,0 +1,92 @@ +import { describe, test, expect, beforeEach, afterAll } from "bun:test"; +import { createSessionRepository } from "@repositories/SessionRepository"; +import { createUserRepository } from "@repositories/UserRepository"; +import { prisma } from "@lib/prisma"; + +/** + * SessionRepository Integration Test + * Validates session lifecycle, SHA-256 hashing, and relational integrity. + */ +describe("SessionRepository Integration Test", () => { + const sessionRepo = createSessionRepository(prisma); + const userRepo = createUserRepository(prisma); + + // Clean up database to ensure isolation + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + test("should create a session with a hashed secret and return the plain secret", async () => { + // 1. Setup: A session requires a valid User ID due to FK constraints + const user = await userRepo.create({ + id: "SESSU01", + username: "session_user", + password: "password123", + }); + + const sessionData = { + userId: user.id, + ipAddress: "192.168.1.1", + userAgent: "Bun-Runtime", + }; + + // 2. Execution + const session = await sessionRepo.create(sessionData); + + // 3. Assertions + expect(session.id).toBeDefined(); + expect(session.userId).toBe(user.id); + + // Ensure the 'secret' is returned plain (for token generation) + // but the 'secretHash' is what actually lives in the DB + expect(session.secret).toBeDefined(); + expect(session.secret).toHaveLength(30); // 15 bytes hex = 30 chars + expect(session.secretHash).not.toBe(session.secret); + + // Verify DB persistence + const dbSession = await prisma.session.findUnique({ + where: { id: session.id }, + }); + expect(dbSession).not.toBeNull(); + expect(dbSession?.secretHash).toBe(session.secretHash); + }); + + test("should find a session by ID and matching secret hash", async () => { + const user = await userRepo.create({ + id: "AUTHU01", + username: "auth_user", + password: "pwd", + }); + const session = await sessionRepo.create({ userId: user.id }); + + // Attempt to find using the correct hash generated during creation + const found = await sessionRepo.findByIdAndSecretHash(session.id, session.secretHash); + + expect(found).not.toBeNull(); + expect(found?.id).toBe(session.id); + }); + + test("should return null if the secret hash does not match", async () => { + const user = await userRepo.create({ + id: "SECU01A", + username: "secure_user", + password: "pwd", + }); + const session = await sessionRepo.create({ userId: user.id }); + + // Attempt to find using an incorrect hash + const found = await sessionRepo.findByIdAndSecretHash(session.id, "wrong_hash"); + + expect(found).toBeNull(); + }); + + test("should maintain referential integrity (Foreign Key)", async () => { + // Attempting to create a session for a non-existent user should fail at DB level + expect(sessionRepo.create({ userId: "NON_EXISTENT_USER" })).rejects.toThrow(); + }); +}); diff --git a/tests/integration/repositories/UserRepository.test.ts b/tests/integration/repositories/UserRepository.test.ts new file mode 100644 index 0000000..9b705f3 --- /dev/null +++ b/tests/integration/repositories/UserRepository.test.ts @@ -0,0 +1,98 @@ +import { describe, test, expect, beforeEach, afterAll } from "bun:test"; +import { createUserRepository } from "@repositories/UserRepository"; +import { prisma } from "@lib/prisma"; +import { Prisma } from "@prisma/generated/client"; + +/** + * UserRepository Integration Test + * Verifies database interaction for persistence-only repository behavior. + */ +describe("UserRepository Integration Test", () => { + const repo = createUserRepository(prisma); + + // Clean up the user table before each test to ensure a predictable state + beforeEach(async () => { + await prisma.user.deleteMany(); + }); + + // Close database connection after all tests are finished + afterAll(async () => { + await prisma.$disconnect(); + }); + + test("should persist a new user to the database", async () => { + const userData = { + id: "INTEG01", + username: "integration_tester", + password: "hashed_password", + displayName: "Integration Bot", + email: "bot@example.com", + }; + + const user = await repo.create(userData); + + // Verify returned object properties + expect(user.id).toBe(userData.id); + expect(user.username).toBe(userData.username); + expect(user.password).toBe(userData.password); + + // Verify database record actually exists + const dbUser = await prisma.user.findUnique({ where: { id: user.id } }); + expect(dbUser).not.toBeNull(); + expect(dbUser?.username).toBe(userData.username); + expect(dbUser?.password).toBe(userData.password); + }); + + test("should throw Prisma P2002 when username is duplicated", async () => { + const userData = { + username: "duplicate_user", + password: "hashed_password", + }; + + // Create the first user + await repo.create({ ...userData, id: "DUPUSR1" }); + + // Repository now only persists data and leaves business error mapping to service + try { + await repo.create({ ...userData, id: "DUPUSR2" }); + expect.unreachable(); + } catch (e) { + expect(e).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); + const err = e as Prisma.PrismaClientKnownRequestError; + expect(err.code).toBe("P2002"); + } + }); + + test("should find a user by email correctly", async () => { + const email = "search@example.com"; + await repo.create({ + id: "SEARCH1", + username: "searcher", + password: "hashed_password", + email, + }); + + const foundUser = await repo.findByEmail(email); + expect(foundUser).not.toBeNull(); + expect(foundUser?.email).toBe(email); + }); + + test("should find a user by username correctly", async () => { + const username = "searcher"; + await repo.create({ + id: "SEARCH2", + username, + password: "hashed_password", + email: "search@example.com", + }); + + const foundUser = await repo.findByUsername(username); + expect(foundUser).not.toBeNull(); + expect(foundUser?.username).toBe(username); + }); + + test("should return null when searching for non-existent user", async () => { + const user = await repo.findById("NON_EXISTENT_ID"); + expect(user).toBeNull(); + }); +}); diff --git a/tests/unit/lib/conflictError.test.ts b/tests/unit/lib/conflictError.test.ts new file mode 100644 index 0000000..51caa0d --- /dev/null +++ b/tests/unit/lib/conflictError.test.ts @@ -0,0 +1,21 @@ +import { describe, test, expect } from "bun:test"; +import { ConflictError } from "@lib/error/conflict"; + +describe("ConflictError", () => { + test("Has correct default value", () => { + const err = new ConflictError("username"); + + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("APP_CONFLICT_ERROR"); + expect(err.statusCode).toBe(409); + expect(err.meta).toEqual({ field: "username" }); + expect(err.message).toBe("Record already exists"); + }); + + test("Can override code and message", () => { + const err = new ConflictError("email", "EMAIL_DUPLICATED", "Email registered", 409); + + expect(err.code).toBe("EMAIL_DUPLICATED"); + expect(err.message).toBe("Email registered"); + }); +}); diff --git a/tests/unit/services/AuthService.test.ts b/tests/unit/services/AuthService.test.ts new file mode 100644 index 0000000..b73ba4c --- /dev/null +++ b/tests/unit/services/AuthService.test.ts @@ -0,0 +1,166 @@ +import { describe, test, expect, mock, beforeEach } from "bun:test"; +import { AuthService } from "@services/AuthService"; +import type { + IUserRepository, + ISessionRepository, + SessionWithSecret, +} from "@repositories/interfaces"; +import type { User } from "@prisma/generated/client"; +import { Prisma } from "@prisma/generated/client"; + +function hashSecret(secret: string): string { + const hasher = new Bun.CryptoHasher("sha256"); + hasher.update(secret); + return hasher.digest("hex"); +} + +/** + * AuthService Unit Test + * * This suite uses strict typing to ensure that our mocks satisfy + * the IUserRepository and ISessionRepository interfaces without + * resorting to 'any' type casting. + */ +describe("AuthService Unit Test (Strict Types)", () => { + let mockUserRepo: IUserRepository; + let mockSessionRepo: ISessionRepository; + let authService: AuthService; + + // Standardized mock data matching Prisma generated types + const mockUser: User = { + id: "BX23Q95", + username: "testuser", + password: "hashed_password", + displayName: "Test User", + email: "test@example.com", + roleId: null, + reputation: 0, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + }; + + const mockSession: SessionWithSecret = { + id: "0123456789abcdef0123456789abcd", + userId: "BX23Q95", + secretHash: hashSecret("abcdef0123456789abcdef01234567"), + secret: "abcdef0123456789abcdef01234567", + userAgent: "Mozilla/5.0", + ipAddress: "127.0.0.1", + createdAt: new Date(), + lastVerifiedAt: new Date(), + }; + + beforeEach(() => { + // We define the mocks explicitly to satisfy the interface requirements + mockUserRepo = { + create: mock(async () => mockUser), + findById: mock(async (id: string) => (id === mockUser.id ? mockUser : null)), + findByUsername: mock(async () => null), + findByEmail: mock(async () => null), + }; + + mockSessionRepo = { + create: mock(async () => mockSession), + findById: mock(async (id: string) => (id === mockSession.id ? mockSession : null)), + findByIdAndSecretHash: mock(async (id: string, secretHash: string) => + id === mockSession.id && secretHash === mockSession.secretHash ? mockSession : null + ), + }; + + // Inject the strictly typed mocks into the service + authService = new AuthService(mockUserRepo, mockSessionRepo); + }); + + describe("register()", () => { + test("should register user and return token without type assertions", async () => { + const signupData = { + username: "testuser", + password: "securePassword123", + displayName: "Test User", + }; + + const result = await authService.register(signupData, "127.0.0.1", "Bun-Test"); + + // Verify the logic flow matches our architectural requirements + expect(mockUserRepo.create).toHaveBeenCalled(); + expect(mockSessionRepo.create).toHaveBeenCalled(); + expect(result.token).toBe(`${mockSession.id}.${mockSession.secret}`); + expect(result.user.id).toBe(mockUser.id); + }); + + test("should execute password verification padding before surfacing duplicate username errors", async () => { + const duplicateError = new Prisma.PrismaClientKnownRequestError( + "Unique constraint failed", + { + code: "P2002", + clientVersion: "test", + meta: { + driverAdapterError: { + cause: { + constraint: { + fields: ["username"], + }, + }, + }, + }, + } + ); + + const verifySpy = mock(async () => true); + + mockUserRepo.create = mock(async () => { + throw duplicateError; + }); + authService = new AuthService(mockUserRepo, mockSessionRepo, undefined, { + passwordVerify: verifySpy, + }); + + expect( + authService.register({ + username: "testuser", + password: "securePassword123", + }) + ).rejects.toMatchObject({ + code: "USERNAME_DUPLICATED", + }); + + expect(verifySpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("verifyToken()", () => { + test("should return user for valid token format", async () => { + const validToken = `${mockSession.id}.${mockSession.secret}`; + + const result = await authService.verifyToken(validToken); + + // Ensure the service correctly hashes the secret and queries repositories + expect(result).not.toBeNull(); + expect(result?.id).toBe(mockUser.id); + expect(mockSessionRepo.findById).toHaveBeenCalledWith(mockSession.id); + expect(mockSessionRepo.findByIdAndSecretHash).not.toHaveBeenCalled(); + }); + + test("should return null for malformed tokens after performing padded lookup", async () => { + const result = await authService.verifyToken("invalid-format"); + expect(result).toBeNull(); + expect(mockSessionRepo.findById).toHaveBeenCalledTimes(1); + }); + + test("should return null if repository finds no session", async () => { + mockSessionRepo.findById = mock(async () => null); + authService = new AuthService(mockUserRepo, mockSessionRepo); + + const result = await authService.verifyToken(`${mockSession.id}.${mockSession.secret}`); + expect(result).toBeNull(); + }); + + test("should return null when secret does not match an existing session", async () => { + const result = await authService.verifyToken( + `${mockSession.id}.000000000000000000000000000000` + ); + expect(result).toBeNull(); + expect(mockUserRepo.findById).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..9ccda83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,17 @@ { "compilerOptions": { + "paths": { + "@prisma/*": ["./prisma/*"], + "@routes/*": ["./src/routes/*"], + "@lib/*": ["./src/lib/*"], + "@repositories/*": ["./src/repositories/*"], + "@services/*": ["./src/services/*"], + "@controllers/*": ["./src/controllers/*"], + "@schemas/*": ["./src/schemas/*"], + "@plugins/*": ["./src/plugins/*"], + "@/*": ["./src/*"] + }, + // Environment setup & latest features "lib": ["ESNext"], "target": "ESNext", From 7b3904f15c9197fe5d4bca2d9ad8d6120fcbeb7c Mon Sep 17 00:00:00 2001 From: alikia2x Date: Tue, 10 Mar 2026 03:00:54 +0800 Subject: [PATCH 02/37] refactor: reorganize repo into turbo monorepo (#6) --- .gitignore | 4 +- README.md | 130 ++++++++---------- apps/backend/.env.example | 1 + apps/backend/README.md | 108 +++++++++++++++ apps/backend/package.json | 36 +++++ {src => apps/backend/src}/containers.ts | 0 {src => apps/backend/src}/index.ts | 0 {src => apps/backend/src}/lib/env.ts | 0 .../backend/src}/lib/error/conflict.ts | 0 {src => apps/backend/src}/lib/error/index.ts | 0 .../backend/src}/lib/error/rateLimit.ts | 0 {src => apps/backend/src}/lib/prisma.ts | 2 +- {src => apps/backend/src}/onAfterHandle.ts | 0 .../src}/repositories/SessionRepository.ts | 2 +- .../src}/repositories/UserRepository.ts | 4 +- .../backend/src}/repositories/index.ts | 0 .../interfaces/ISessionRepository.ts | 2 +- .../interfaces/IUserRepository.ts | 2 +- .../src}/repositories/interfaces/index.ts | 0 .../backend/src}/routes/auth/currentUser.ts | 0 .../backend/src}/routes/auth/index.ts | 0 .../backend/src}/routes/auth/signup.ts | 0 .../backend/src}/schemas/auth/currentUser.ts | 0 .../backend/src}/schemas/auth/index.ts | 0 .../backend/src}/schemas/auth/signup.ts | 0 .../schemas/common/ErrorResponseSchema.ts | 0 .../backend/src}/schemas/common/index.ts | 0 .../backend/src}/services/AuthService.ts | 2 +- {src => apps/backend/src}/services/index.ts | 0 .../src}/services/interfaces/IAuthService.ts | 2 +- .../backend/src}/services/interfaces/index.ts | 0 {src => apps/backend/src}/startMessage.ts | 0 .../backend/tests}/e2e/auth.test.ts | 0 .../repositories/SessionRepository.test.ts | 0 .../repositories/UserRepository.test.ts | 2 +- .../tests}/unit/lib/conflictError.test.ts | 0 .../tests}/unit/services/AuthService.test.ts | 4 +- apps/backend/tsconfig.json | 15 ++ bun.lock | 117 +++++++++++----- package.json | 55 ++------ packages/config/package.json | 6 + packages/config/tsconfig/base.json | 12 ++ packages/config/tsconfig/dom.json | 11 ++ packages/config/tsconfig/server.json | 11 ++ packages/db/.env.example | 1 + packages/db/.gitignore | 1 + packages/db/README.md | 11 ++ packages/db/index.ts | 3 + packages/db/package.json | 26 ++++ .../db/prisma.config.ts | 0 .../20260227101459_init/migration.sql | 0 .../db/prisma}/migrations/migration_lock.toml | 0 .../db/prisma}/models/auth/README.md | 0 .../db/prisma}/models/auth/session.prisma | 0 .../db/prisma}/models/auth/user.prisma | 0 .../db/prisma}/models/core/README.md | 0 .../db/prisma}/models/core/album.prisma | 0 .../db/prisma}/models/core/artist.prisma | 0 .../db/prisma}/models/core/artistRole.prisma | 0 .../prisma}/models/core/externalLink.prisma | 0 .../db/prisma}/models/core/group.prisma | 0 .../db/prisma}/models/core/lyrics.prisma | 0 .../db/prisma}/models/core/singer.prisma | 0 .../db/prisma}/models/core/song.prisma | 0 .../db/prisma}/models/core/songSeries.prisma | 0 .../db/prisma}/models/core/svsEngine.prisma | 0 .../models/core/svsEngineVersion.prisma | 0 .../db/prisma}/models/core/tag.prisma | 0 .../db/prisma}/models/core/voicebank.prisma | 0 .../db/prisma}/models/meta/README.md | 0 .../db/prisma}/models/meta/history.prisma | 0 .../db/prisma}/models/meta/permission.prisma | 0 .../db/prisma}/models/meta/role.prisma | 0 .../db/prisma}/models/models.prisma | 0 .../db/prisma}/models/platform/README.md | 0 .../db/prisma}/models/platform/file.prisma | 0 .../db/prisma}/models/platform/post.prisma | 0 .../models/platform/reputationHistory.prisma | 0 .../db/prisma}/relations/README.md | 0 .../relations/core/singerOfSong.prisma | 0 .../relations/core/singerSvsEngine.prisma | 0 .../core/singerSvsEngineVersion.prisma | 0 .../prisma}/relations/core/songInAlbum.prisma | 0 .../relations/core/songInSeries.prisma | 0 .../db/prisma}/relations/core/songTag.prisma | 0 .../relations/meta/rolePermission.prisma | 0 .../db/prisma}/relations/relations.prisma | 0 {prisma => packages/db/prisma}/schema.prisma | 0 {prisma => packages/db/prisma}/types.ts | 0 tsconfig.json | 41 ------ turbo.json | 15 ++ 91 files changed, 425 insertions(+), 201 deletions(-) create mode 100644 apps/backend/.env.example create mode 100644 apps/backend/README.md create mode 100644 apps/backend/package.json rename {src => apps/backend/src}/containers.ts (100%) rename {src => apps/backend/src}/index.ts (100%) rename {src => apps/backend/src}/lib/env.ts (100%) rename {src => apps/backend/src}/lib/error/conflict.ts (100%) rename {src => apps/backend/src}/lib/error/index.ts (100%) rename {src => apps/backend/src}/lib/error/rateLimit.ts (100%) rename {src => apps/backend/src}/lib/prisma.ts (78%) rename {src => apps/backend/src}/onAfterHandle.ts (100%) rename {src => apps/backend/src}/repositories/SessionRepository.ts (95%) rename {src => apps/backend/src}/repositories/UserRepository.ts (92%) rename {src => apps/backend/src}/repositories/index.ts (100%) rename {src => apps/backend/src}/repositories/interfaces/ISessionRepository.ts (89%) rename {src => apps/backend/src}/repositories/interfaces/IUserRepository.ts (89%) rename {src => apps/backend/src}/repositories/interfaces/index.ts (100%) rename {src => apps/backend/src}/routes/auth/currentUser.ts (100%) rename {src => apps/backend/src}/routes/auth/index.ts (100%) rename {src => apps/backend/src}/routes/auth/signup.ts (100%) rename {src => apps/backend/src}/schemas/auth/currentUser.ts (100%) rename {src => apps/backend/src}/schemas/auth/index.ts (100%) rename {src => apps/backend/src}/schemas/auth/signup.ts (100%) rename {src => apps/backend/src}/schemas/common/ErrorResponseSchema.ts (100%) rename {src => apps/backend/src}/schemas/common/index.ts (100%) rename {src => apps/backend/src}/services/AuthService.ts (98%) rename {src => apps/backend/src}/services/index.ts (100%) rename {src => apps/backend/src}/services/interfaces/IAuthService.ts (87%) rename {src => apps/backend/src}/services/interfaces/index.ts (100%) rename {src => apps/backend/src}/startMessage.ts (100%) rename {tests => apps/backend/tests}/e2e/auth.test.ts (100%) rename {tests => apps/backend/tests}/integration/repositories/SessionRepository.test.ts (100%) rename {tests => apps/backend/tests}/integration/repositories/UserRepository.test.ts (98%) rename {tests => apps/backend/tests}/unit/lib/conflictError.test.ts (100%) rename {tests => apps/backend/tests}/unit/services/AuthService.test.ts (97%) create mode 100644 apps/backend/tsconfig.json create mode 100644 packages/config/package.json create mode 100644 packages/config/tsconfig/base.json create mode 100644 packages/config/tsconfig/dom.json create mode 100644 packages/config/tsconfig/server.json create mode 100644 packages/db/.env.example create mode 100644 packages/db/.gitignore create mode 100644 packages/db/README.md create mode 100644 packages/db/index.ts create mode 100644 packages/db/package.json rename prisma.config.ts => packages/db/prisma.config.ts (100%) rename {prisma => packages/db/prisma}/migrations/20260227101459_init/migration.sql (100%) rename {prisma => packages/db/prisma}/migrations/migration_lock.toml (100%) rename {prisma => packages/db/prisma}/models/auth/README.md (100%) rename {prisma => packages/db/prisma}/models/auth/session.prisma (100%) rename {prisma => packages/db/prisma}/models/auth/user.prisma (100%) rename {prisma => packages/db/prisma}/models/core/README.md (100%) rename {prisma => packages/db/prisma}/models/core/album.prisma (100%) rename {prisma => packages/db/prisma}/models/core/artist.prisma (100%) rename {prisma => packages/db/prisma}/models/core/artistRole.prisma (100%) rename {prisma => packages/db/prisma}/models/core/externalLink.prisma (100%) rename {prisma => packages/db/prisma}/models/core/group.prisma (100%) rename {prisma => packages/db/prisma}/models/core/lyrics.prisma (100%) rename {prisma => packages/db/prisma}/models/core/singer.prisma (100%) rename {prisma => packages/db/prisma}/models/core/song.prisma (100%) rename {prisma => packages/db/prisma}/models/core/songSeries.prisma (100%) rename {prisma => packages/db/prisma}/models/core/svsEngine.prisma (100%) rename {prisma => packages/db/prisma}/models/core/svsEngineVersion.prisma (100%) rename {prisma => packages/db/prisma}/models/core/tag.prisma (100%) rename {prisma => packages/db/prisma}/models/core/voicebank.prisma (100%) rename {prisma => packages/db/prisma}/models/meta/README.md (100%) rename {prisma => packages/db/prisma}/models/meta/history.prisma (100%) rename {prisma => packages/db/prisma}/models/meta/permission.prisma (100%) rename {prisma => packages/db/prisma}/models/meta/role.prisma (100%) rename {prisma => packages/db/prisma}/models/models.prisma (100%) rename {prisma => packages/db/prisma}/models/platform/README.md (100%) rename {prisma => packages/db/prisma}/models/platform/file.prisma (100%) rename {prisma => packages/db/prisma}/models/platform/post.prisma (100%) rename {prisma => packages/db/prisma}/models/platform/reputationHistory.prisma (100%) rename {prisma => packages/db/prisma}/relations/README.md (100%) rename {prisma => packages/db/prisma}/relations/core/singerOfSong.prisma (100%) rename {prisma => packages/db/prisma}/relations/core/singerSvsEngine.prisma (100%) rename {prisma => packages/db/prisma}/relations/core/singerSvsEngineVersion.prisma (100%) rename {prisma => packages/db/prisma}/relations/core/songInAlbum.prisma (100%) rename {prisma => packages/db/prisma}/relations/core/songInSeries.prisma (100%) rename {prisma => packages/db/prisma}/relations/core/songTag.prisma (100%) rename {prisma => packages/db/prisma}/relations/meta/rolePermission.prisma (100%) rename {prisma => packages/db/prisma}/relations/relations.prisma (100%) rename {prisma => packages/db/prisma}/schema.prisma (100%) rename {prisma => packages/db/prisma}/types.ts (100%) delete mode 100644 tsconfig.json create mode 100644 turbo.json diff --git a/.gitignore b/.gitignore index 9e09f9b..0e35deb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -prisma/generated +data -data \ No newline at end of file +.turbo \ No newline at end of file diff --git a/README.md b/README.md index f1fc599..edf28a9 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,104 @@ -# CVSA Backend +# CVSA -The backend service for the **Chinese Vocal Synthesizer Archive (CVSA)** – a unified platform for the Chinese vocal synthesizer community. -This service provides a RESTful API for the rest of the CVSA system and third parties to interact with. +The official monorepo for the **Chinese Vocal Synthesizer Archive (CVSA)** – a unified platform that aggregates and preserves information about the Chinese vocal synthesizer community. + +## Overview + +CVSA is a community-driven archive that consolidates scattered information about Chinese singing voice synthesis community. + +This monorepo contains all core components of the CVSA platform, managed with [Turborepo](https://turbo.build/repo) and [Bun](https://bun.sh). + +## Project Structure + +``` +cvsa/ +├── apps/ +│ ├── backend/ # Core REST API service (Elysia + Prisma) +│ └── frontend/ # Astro SSR application with React islands (Not implemented yet) +├── packages/ +│ ├── config/ # Shared configuration (TypeScript, etc.) +│ └── db/ # Prisma schema and database client +├── .github/ # GitHub workflows and issue templates +└── turbo.json # Turborepo configuration +``` ## Tech Stack -| Layer | Technology | -| ----------- | --------------------------------------------------------------------------------------- | -| Runtime | [Bun](https://bun.sh) – fast JavaScript runtime, package manager, and test runner | -| Framework | [Elysia](https://elysiajs.com) – high‑performance, type‑friendly web framework | -| Database | PostgreSQL | -| ORM | [Prisma](https://www.prisma.io) – type‑safe database client with migrations | -| Testing | Bun test (unit, integration, E2E) | +| Layer | Technology | Purpose | +| -------------- | ----------------------------------------------------- | ----------------------------------------------------- | +| **Runtime** | [Bun](https://bun.sh) | Fast JavaScript runtime, package manager, test runner | +| **Monorepo** | [Turborepo](https://turbo.build) | Build system and task orchestration | +| **Frontend** | [Astro](https://astro.build) + React | SSR with partial hydration (islands architecture) | +| **Backend** | [Elysia](https://elysiajs.com) | High-performance, type-safe API framework | +| **Database** | PostgreSQL + [TimescaleDB](https://www.timescale.com) | Core storage with time-series optimization | +| **ORM** | [Prisma](https://www.prisma.io) | Type-safe database client and migrations | ## Getting Started ### Prerequisites - [Bun](https://bun.sh) >= 1.3.10 -- PostgreSQL >= 18 +- [PostgreSQL](https://www.postgresql.org) >= 18 -### Installation +### Initial Setup 1. **Clone the repository** ```bash - git clone https://github.com/project-cvsa/cvsa-backend - cd cvsa-backend + git clone https://github.com/project-cvsa/cvsa + cd cvsa ``` 2. **Install dependencies** ```bash - bun install + bun i ``` -3. **Set up environment variables** - Copy the example file and edit it with your database credentials: - - ```bash - cp .env.example .env - ``` - - Required variables: - - ```ini - DATABASE_URL="postgresql://user:password@localhost:5432/cvsa" - ``` +3. **Set up environment variables** -4. **Generate Prisma client and run migrations** + Create `.env` files in each service directory following their `.env.example` templates: ```bash - bun db-gen # generate Prisma Client - bun migrate:dev # apply migrations to your database + cp apps/frontend/.env.example apps/frontend/.env + cp packages/db/.env.example packages/db/.env ``` - (For production, use `bun migrate` with custom scripts.) - -5. **Start the development server** +4. **Initialize the database** ```bash - bun dev - ``` + cd packages/db - The server will start at `http://localhost:16412` (configurable via `PORT`). + # Generate Prisma client + bun db:gen -## Testing + # Run migrations + bun db:migrate:dev + ``` -We use Bun’s built‑in test runner. +### Development -Run all tests: +Start all services in development mode: ```bash -bun test +bun dev ``` -For a specific suite: +## Available Scripts -```bash -bun test:unit -bun test:integration -bun test:e2e -``` +| Command | Description | +| -------------- | -------------------------------------- | +| `bun dev` | Start all services in development mode | +| `bun build` | Build all applications | +| `bun lint` | Run Biome linter across the codebase | +| `bun lint:fix` | Fix linting issues automatically | +| `bun format` | Format code with Biome | +| `bun test` | Run tests across all packages | -To set up the test database (requires `DATABASE_URL` pointing to a test database): +### Package-Specific Scripts -```bash -bun test:db:setup # runs migrations and resets the test DB -``` - - -## Project Structure - -``` -src/ -├── containers.ts # DI container wiring -├── index.ts # App entry point -├── onAfterHandle.ts # Global response handler -├── startMessage.ts # Startup logging -├── lib/ # Shared utilities -├── repositories/ # Data access layer -├── services/ # Business logic -├── routes/ # Elysia route definitions -└── schemas/ # Zod validation schemas -tests/ # Unit, integration, E2E tests -prisma/ # Prisma schema and migrations -``` +See each package's `README.md` for available scripts. ## License -This project is licensed under the AGPLv3 License – see the [LICENSE](LICENSE) file for details. +This project is licensed under the **GNU Affero General Public License v3.0** – see the [LICENSE](LICENSE) file for details. diff --git a/apps/backend/.env.example b/apps/backend/.env.example new file mode 100644 index 0000000..701c7b7 --- /dev/null +++ b/apps/backend/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..ab98466 --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,108 @@ +# CVSA Backend + +The backend service for the **Chinese Vocal Synthesizer Archive (CVSA)** – a unified platform for the Chinese vocal synthesizer community. +This service provides a RESTful API for the rest of the CVSA system and third parties to interact with. + +## Tech Stack + +| Layer | Technology | +| ----------- | --------------------------------------------------------------------------------------- | +| Runtime | [Bun](https://bun.sh) – fast JavaScript runtime, package manager, and test runner | +| Framework | [Elysia](https://elysiajs.com) – high‑performance, type‑friendly web framework | +| Database | PostgreSQL | +| ORM | [Prisma](https://www.prisma.io) – type‑safe database client with migrations | +| Testing | Bun test (unit, integration, E2E) | + +## Getting Started + +### Prerequisites + +- [Bun](https://bun.sh) >= 1.3.10 +- PostgreSQL >= 18 + +### Installation + +1. **Clone the repository** + + ```bash + git clone https://github.com/project-cvsa/cvsa-backend + cd cvsa-backend + ``` + +2. **Install dependencies** + + ```bash + bun install + ``` + +3. **Set up environment variables** + Copy the example file and edit it with your database credentials: + + ```bash + cp .env.example .env + ``` + + Required variables: + + ```ini + DATABASE_URL="postgresql://user:password@localhost:5432/cvsa" + ``` + +4. **Generate Prisma client and run migrations** + + ```bash + bun db-gen # generate Prisma Client + bun migrate:dev # apply migrations to your database + ``` + + (For production, use `bun migrate` with custom scripts.) + +5. **Start the development server** + + ```bash + bun dev + ``` + + The server will start at `http://localhost:16412` (configurable via `PORT`). + +## Testing + +We use Bun’s built‑in test runner. + +Run all tests: + +```bash +bun test +``` + +For a specific suite: + +```bash +bun test:unit +bun test:integration +bun test:e2e +``` + +To set up the test database (requires `DATABASE_URL` pointing to a test database): + +```bash +bun test:db:setup # runs migrations and resets the test DB +``` + + +## Package Structure + +``` +src/ +├── containers.ts # DI container wiring +├── index.ts # App entry point +├── onAfterHandle.ts # Global response handler +├── startMessage.ts # Startup logging +├── lib/ # Shared utilities +├── repositories/ # Data access layer +├── services/ # Business logic +├── routes/ # Elysia route definitions +└── schemas/ # Zod validation schemas +tests/ # Unit, integration, E2E tests +``` + diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..53de1ae --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,36 @@ +{ + "name": "@project-cvsa/backend", + "type": "module", + "version": "0.0.1", + "scripts": { + "lint": "bunx --bun biome lint", + "dev": "bun --hot src/index.ts", + "start": "bun src/index.ts", + "test": "bun test", + "test:coverage": "bun test --coverage", + "test:unit": "bun test tests/unit", + "test:e2e": "bun test tests/e2e", + "test:integration": "bun test tests/integration" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.4", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@elysiajs/eden": "^1.0.5", + "@prisma/adapter-pg": "^7.4.1", + "@t3-oss/env-core": "^0.13.10", + "argon2": "^0.44.0", + "chalk": "^5.6.2", + "elysia": "^1.4.26", + "elysia-ip": "^1.0.10", + "elysia-rate-limit": "^4.5.0", + "nanoid": "^5.1.6", + "zod": "^4.3.6", + "@project-cvsa/db": "workspace:*", + "@project-cvsa/configs": "workspace:*" + } +} diff --git a/src/containers.ts b/apps/backend/src/containers.ts similarity index 100% rename from src/containers.ts rename to apps/backend/src/containers.ts diff --git a/src/index.ts b/apps/backend/src/index.ts similarity index 100% rename from src/index.ts rename to apps/backend/src/index.ts diff --git a/src/lib/env.ts b/apps/backend/src/lib/env.ts similarity index 100% rename from src/lib/env.ts rename to apps/backend/src/lib/env.ts diff --git a/src/lib/error/conflict.ts b/apps/backend/src/lib/error/conflict.ts similarity index 100% rename from src/lib/error/conflict.ts rename to apps/backend/src/lib/error/conflict.ts diff --git a/src/lib/error/index.ts b/apps/backend/src/lib/error/index.ts similarity index 100% rename from src/lib/error/index.ts rename to apps/backend/src/lib/error/index.ts diff --git a/src/lib/error/rateLimit.ts b/apps/backend/src/lib/error/rateLimit.ts similarity index 100% rename from src/lib/error/rateLimit.ts rename to apps/backend/src/lib/error/rateLimit.ts diff --git a/src/lib/prisma.ts b/apps/backend/src/lib/prisma.ts similarity index 78% rename from src/lib/prisma.ts rename to apps/backend/src/lib/prisma.ts index 676b8c6..c231dc1 100644 --- a/src/lib/prisma.ts +++ b/apps/backend/src/lib/prisma.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from "@prisma/generated/client"; +import { PrismaClient } from "@project-cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; import { env } from "@lib/env"; diff --git a/src/onAfterHandle.ts b/apps/backend/src/onAfterHandle.ts similarity index 100% rename from src/onAfterHandle.ts rename to apps/backend/src/onAfterHandle.ts diff --git a/src/repositories/SessionRepository.ts b/apps/backend/src/repositories/SessionRepository.ts similarity index 95% rename from src/repositories/SessionRepository.ts rename to apps/backend/src/repositories/SessionRepository.ts index b497114..9257ea5 100644 --- a/src/repositories/SessionRepository.ts +++ b/apps/backend/src/repositories/SessionRepository.ts @@ -1,6 +1,6 @@ import { prisma } from "@lib/prisma"; import type { ISessionRepository, CreateSessionData, SessionWithSecret } from "./interfaces"; -import type { Session, Prisma, PrismaClient } from "@prisma/generated/client"; +import type { Session, Prisma, PrismaClient } from "@project-cvsa/db"; import crypto from "node:crypto"; export class SessionRepository implements ISessionRepository { diff --git a/src/repositories/UserRepository.ts b/apps/backend/src/repositories/UserRepository.ts similarity index 92% rename from src/repositories/UserRepository.ts rename to apps/backend/src/repositories/UserRepository.ts index d700ef7..9ef419e 100644 --- a/src/repositories/UserRepository.ts +++ b/apps/backend/src/repositories/UserRepository.ts @@ -1,7 +1,7 @@ import { prisma } from "@lib/prisma"; import type { IUserRepository, CreateUserData } from "./interfaces"; -import type { User } from "@prisma/generated/client"; -import type { Prisma, PrismaClient } from "@prisma/generated/client"; +import type { User } from "@project-cvsa/db"; +import type { Prisma, PrismaClient } from "@project-cvsa/db"; export class UserRepository implements IUserRepository { private prisma: PrismaClient; diff --git a/src/repositories/index.ts b/apps/backend/src/repositories/index.ts similarity index 100% rename from src/repositories/index.ts rename to apps/backend/src/repositories/index.ts diff --git a/src/repositories/interfaces/ISessionRepository.ts b/apps/backend/src/repositories/interfaces/ISessionRepository.ts similarity index 89% rename from src/repositories/interfaces/ISessionRepository.ts rename to apps/backend/src/repositories/interfaces/ISessionRepository.ts index 7c84823..f4c1b49 100644 --- a/src/repositories/interfaces/ISessionRepository.ts +++ b/apps/backend/src/repositories/interfaces/ISessionRepository.ts @@ -1,4 +1,4 @@ -import type { Session, Prisma } from "@prisma/generated/client"; +import type { Session, Prisma } from "@project-cvsa/db"; export interface CreateSessionData { userId: string; diff --git a/src/repositories/interfaces/IUserRepository.ts b/apps/backend/src/repositories/interfaces/IUserRepository.ts similarity index 89% rename from src/repositories/interfaces/IUserRepository.ts rename to apps/backend/src/repositories/interfaces/IUserRepository.ts index 330090f..1bd461a 100644 --- a/src/repositories/interfaces/IUserRepository.ts +++ b/apps/backend/src/repositories/interfaces/IUserRepository.ts @@ -1,4 +1,4 @@ -import type { User, Prisma } from "@prisma/generated/client"; +import type { User, Prisma } from "@project-cvsa/db"; export interface CreateUserData { id: string; diff --git a/src/repositories/interfaces/index.ts b/apps/backend/src/repositories/interfaces/index.ts similarity index 100% rename from src/repositories/interfaces/index.ts rename to apps/backend/src/repositories/interfaces/index.ts diff --git a/src/routes/auth/currentUser.ts b/apps/backend/src/routes/auth/currentUser.ts similarity index 100% rename from src/routes/auth/currentUser.ts rename to apps/backend/src/routes/auth/currentUser.ts diff --git a/src/routes/auth/index.ts b/apps/backend/src/routes/auth/index.ts similarity index 100% rename from src/routes/auth/index.ts rename to apps/backend/src/routes/auth/index.ts diff --git a/src/routes/auth/signup.ts b/apps/backend/src/routes/auth/signup.ts similarity index 100% rename from src/routes/auth/signup.ts rename to apps/backend/src/routes/auth/signup.ts diff --git a/src/schemas/auth/currentUser.ts b/apps/backend/src/schemas/auth/currentUser.ts similarity index 100% rename from src/schemas/auth/currentUser.ts rename to apps/backend/src/schemas/auth/currentUser.ts diff --git a/src/schemas/auth/index.ts b/apps/backend/src/schemas/auth/index.ts similarity index 100% rename from src/schemas/auth/index.ts rename to apps/backend/src/schemas/auth/index.ts diff --git a/src/schemas/auth/signup.ts b/apps/backend/src/schemas/auth/signup.ts similarity index 100% rename from src/schemas/auth/signup.ts rename to apps/backend/src/schemas/auth/signup.ts diff --git a/src/schemas/common/ErrorResponseSchema.ts b/apps/backend/src/schemas/common/ErrorResponseSchema.ts similarity index 100% rename from src/schemas/common/ErrorResponseSchema.ts rename to apps/backend/src/schemas/common/ErrorResponseSchema.ts diff --git a/src/schemas/common/index.ts b/apps/backend/src/schemas/common/index.ts similarity index 100% rename from src/schemas/common/index.ts rename to apps/backend/src/schemas/common/index.ts diff --git a/src/services/AuthService.ts b/apps/backend/src/services/AuthService.ts similarity index 98% rename from src/services/AuthService.ts rename to apps/backend/src/services/AuthService.ts index 5db5cdd..3949f8e 100644 --- a/src/services/AuthService.ts +++ b/apps/backend/src/services/AuthService.ts @@ -4,7 +4,7 @@ import type { ISessionRepository } from "@repositories/interfaces/ISessionReposi import { prisma } from "@lib/prisma"; import { AppError } from "@lib/error"; import { ConflictError } from "@lib/error/conflict"; -import { Prisma, type User, type PrismaClient } from "@prisma/generated/client"; +import { Prisma, type User, type PrismaClient } from "@project-cvsa/db"; import { customAlphabet } from "nanoid"; import crypto from "node:crypto"; diff --git a/src/services/index.ts b/apps/backend/src/services/index.ts similarity index 100% rename from src/services/index.ts rename to apps/backend/src/services/index.ts diff --git a/src/services/interfaces/IAuthService.ts b/apps/backend/src/services/interfaces/IAuthService.ts similarity index 87% rename from src/services/interfaces/IAuthService.ts rename to apps/backend/src/services/interfaces/IAuthService.ts index 21e8fbf..70ece2f 100644 --- a/src/services/interfaces/IAuthService.ts +++ b/apps/backend/src/services/interfaces/IAuthService.ts @@ -1,4 +1,4 @@ -import type { User } from "@prisma/generated/client"; +import type { User } from "@project-cvsa/db"; export interface CreateUserData { username: string; diff --git a/src/services/interfaces/index.ts b/apps/backend/src/services/interfaces/index.ts similarity index 100% rename from src/services/interfaces/index.ts rename to apps/backend/src/services/interfaces/index.ts diff --git a/src/startMessage.ts b/apps/backend/src/startMessage.ts similarity index 100% rename from src/startMessage.ts rename to apps/backend/src/startMessage.ts diff --git a/tests/e2e/auth.test.ts b/apps/backend/tests/e2e/auth.test.ts similarity index 100% rename from tests/e2e/auth.test.ts rename to apps/backend/tests/e2e/auth.test.ts diff --git a/tests/integration/repositories/SessionRepository.test.ts b/apps/backend/tests/integration/repositories/SessionRepository.test.ts similarity index 100% rename from tests/integration/repositories/SessionRepository.test.ts rename to apps/backend/tests/integration/repositories/SessionRepository.test.ts diff --git a/tests/integration/repositories/UserRepository.test.ts b/apps/backend/tests/integration/repositories/UserRepository.test.ts similarity index 98% rename from tests/integration/repositories/UserRepository.test.ts rename to apps/backend/tests/integration/repositories/UserRepository.test.ts index 9b705f3..cdefbc9 100644 --- a/tests/integration/repositories/UserRepository.test.ts +++ b/apps/backend/tests/integration/repositories/UserRepository.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeEach, afterAll } from "bun:test"; import { createUserRepository } from "@repositories/UserRepository"; import { prisma } from "@lib/prisma"; -import { Prisma } from "@prisma/generated/client"; +import { Prisma } from "@project-cvsa/db"; /** * UserRepository Integration Test diff --git a/tests/unit/lib/conflictError.test.ts b/apps/backend/tests/unit/lib/conflictError.test.ts similarity index 100% rename from tests/unit/lib/conflictError.test.ts rename to apps/backend/tests/unit/lib/conflictError.test.ts diff --git a/tests/unit/services/AuthService.test.ts b/apps/backend/tests/unit/services/AuthService.test.ts similarity index 97% rename from tests/unit/services/AuthService.test.ts rename to apps/backend/tests/unit/services/AuthService.test.ts index b73ba4c..13b0777 100644 --- a/tests/unit/services/AuthService.test.ts +++ b/apps/backend/tests/unit/services/AuthService.test.ts @@ -5,8 +5,8 @@ import type { ISessionRepository, SessionWithSecret, } from "@repositories/interfaces"; -import type { User } from "@prisma/generated/client"; -import { Prisma } from "@prisma/generated/client"; +import type { User } from "@project-cvsa/db"; +import { Prisma } from "@project-cvsa/db"; function hashSecret(secret: string): string { const hasher = new Bun.CryptoHasher("sha256"); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..d1d8541 --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@project-cvsa/configs/tsconfig/server.json", + "compilerOptions": { + "paths": { + "@routes/*": ["./src/routes/*"], + "@lib/*": ["./src/lib/*"], + "@repositories/*": ["./src/repositories/*"], + "@services/*": ["./src/services/*"], + "@controllers/*": ["./src/controllers/*"], + "@schemas/*": ["./src/schemas/*"], + "@plugins/*": ["./src/plugins/*"], + "@/*": ["./src/*"] + }, + } +} diff --git a/bun.lock b/bun.lock index 3f45aa5..a93320c 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,19 @@ "workspaces": { "": { "name": "@project-cvsa/backend", + "devDependencies": { + "turbo": "latest", + }, + }, + "apps/backend": { + "name": "@project-cvsa/backend", + "version": "0.0.1", "dependencies": { "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", - "@prisma/client": "^7.4.1", + "@project-cvsa/configs": "workspace:*", + "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", - "@types/pg": "^8.16.0", "argon2": "^0.44.0", "chalk": "^5.6.2", "elysia": "^1.4.26", @@ -21,6 +28,22 @@ "devDependencies": { "@biomejs/biome": "^2.4.4", "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + "packages/config": { + "name": "@project-cvsa/configs", + }, + "packages/db": { + "name": "@project-cvsa/db", + "dependencies": { + "@prisma/adapter-pg": "^7.4.1", + "@prisma/client": "^7.4.1", + "@types/pg": "^8.16.0", + }, + "devDependencies": { "prisma": "^7.4.1", "prisma-json-types-generator": "^4.1.1", }, @@ -32,23 +55,23 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="], + "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], @@ -76,31 +99,31 @@ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.1", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.1", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-AH9XrqvSoBAaStn0Gm/sAnF97pDKz8uLpNmn51j1S9O9dhUva6LIxGdoDiiU9VXRIR89wAJXsvJSy+mK40m2xw=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], - "@prisma/client": ["@prisma/client@7.4.1", "", { "dependencies": { "@prisma/client-runtime-utils": "7.4.1" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-pgIll2W1NVdof37xLeyySW+yfQ4rI+ERGCRwnO3BjVOx42GpYq6jhTyuALK8VKirvJJIvImgfGDA2qwhYVvMuA=="], + "@prisma/client": ["@prisma/client@7.4.2", "", { "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA=="], - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.4.1", "", {}, "sha512-8fy74OMYC7mt9cJ2MncIDk1awPRgmtXVvwTN2FlW4JVhbck8Dgt0wTkhPG85myfj4ZeP2stjF9Sdg12n5HrpQg=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.4.2", "", {}, "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q=="], - "@prisma/config": ["@prisma/config@7.4.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-vteSXm8N46bo3FW9MhPGVHAj+KRgrR6TWtlSk6GqToCKjTnOexXdPZyiDyEsfVW38YhqEmVl6w/6iHN8uYVJcw=="], + "@prisma/config": ["@prisma/config@7.4.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg=="], - "@prisma/debug": ["@prisma/debug@7.4.1", "", {}, "sha512-qEtzO8oLouRv18JDQUC3G3Gnv+fGVscHZm/x1DBB/WT+kOvPDQLM2woX6IGgWnSMYYlrxjuALshT7G/blvY0bQ=="], + "@prisma/debug": ["@prisma/debug@7.4.2", "", {}, "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w=="], "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], - "@prisma/dmmf": ["@prisma/dmmf@7.4.1", "", {}, "sha512-805aCBILKe+l3A/z3JF/X9cH0ZZZegTOfKzm8zRpoBcOhLy+SRM2kGJ4aIXp5a6ENdAh6FN2u7ikZ16UDCI9Kg=="], + "@prisma/dmmf": ["@prisma/dmmf@7.4.2", "", {}, "sha512-EurUPjBwmAd69MM8PLZ5n94xeeQ+7SB3Wd9p2+YsgXhvqCa1cgJRBJiB6fTWkPTz5Lf05ySNJ/59EnuEJMAnrw=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1" } }, "sha512-gEZOC2tnlHaZNbHUdbK8YvQphq2tKq/Ovu1YixJ/hPSutDAvNzC3R+xUeBuJ4AJp236eELMzwxb7rgo3UbRkTg=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw=="], - "@prisma/engines": ["@prisma/engines@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1", "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", "@prisma/fetch-engine": "7.4.1", "@prisma/get-platform": "7.4.1" } }, "sha512-BZEBdHvNJx5PzIG37EI/Zi5UUI5hGWjkYsQmKa7OIK6evAvebOTwutjS/VRI6cA6grmA52eLZR+oekGRMqkKxQ=="], + "@prisma/engines": ["@prisma/engines@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/fetch-engine": "7.4.2", "@prisma/get-platform": "7.4.2" } }, "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA=="], - "@prisma/engines-version": ["@prisma/engines-version@7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", "", {}, "sha512-fUxVd1TjOW8K4XsZ8dAm88sDW5Ry7AxWDfsYEWwScS6Fjo3caKC6hgNumUfsmsy0Il9LjDn5X0PpVXNt3iwayw=="], + "@prisma/engines-version": ["@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "", {}, "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1", "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", "@prisma/get-platform": "7.4.1" } }, "sha512-Z9kbuxX2bvEsyeS3LZEiEnxG0lVtZbpYgaAnPj69N+A9f2De8Lta0EoFtld9zhfERVPIQWhSWUc8himky3qYdA=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/get-platform": "7.4.2" } }, "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA=="], - "@prisma/generator": ["@prisma/generator@7.4.1", "", {}, "sha512-UO9DHDWLvZxAhOZmutOKJUVKLHCqkQze4ZzdOQpO2rbz9s4p7sa/kT1kFKGODIDaGc04T6MziKjsauQVwjKqOQ=="], + "@prisma/generator": ["@prisma/generator@7.4.2", "", {}, "sha512-+4B8Rrq5Ef2cm7cW7Wyu6oXn/QVcDVdNti9mKEkcbPVLlA85I6Rbd+j6r+k2E2nlj0zKZbDbq8fIx/Xnt1SmkA=="], - "@prisma/generator-helper": ["@prisma/generator-helper@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1", "@prisma/dmmf": "7.4.1", "@prisma/generator": "7.4.1" } }, "sha512-moKkU9xHt3S3QAgh4AhVYpAjXSVH3P8N1CBmZ/hepGHSKdVn8aq+viGL942HH+5/t//1bZ0Gd9hUlCjttAn1FQ=="], + "@prisma/generator-helper": ["@prisma/generator-helper@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/dmmf": "7.4.2", "@prisma/generator": "7.4.2" } }, "sha512-PWTkqlMILGVghoWP8SZB3prw/EcthiyPYJsMoiVZrClqPMQn6doPjphCQffPDyGzZvAwmA64edbc6Gm/6Omkjg=="], "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], @@ -108,6 +131,12 @@ "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], + "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], + + "@project-cvsa/configs": ["@project-cvsa/configs@workspace:packages/config"], + + "@project-cvsa/db": ["@project-cvsa/db@workspace:packages/db"], + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -118,11 +147,11 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], + "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], - "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -130,7 +159,7 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -168,7 +197,7 @@ "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], - "elysia": ["elysia@1.4.26", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-oGhEFYwt+4M/U9w34XTdBen2KOQIa/PXne40zUkTWRgJlVJNBhepxOdjWeY1U0CbYoEOyZaj1tc79wOsuCxMYw=="], + "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], @@ -184,7 +213,7 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + "file-type": ["file-type@21.3.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -232,7 +261,7 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -250,17 +279,17 @@ "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], - "pg": ["pg@8.19.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.12.0", "pg-protocol": "^1.12.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ=="], + "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], + "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-pool": ["pg-pool@3.12.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg=="], + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], - "pg-protocol": ["pg-protocol@1.12.0", "", {}, "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg=="], + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], @@ -278,7 +307,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prisma": ["prisma@7.4.1", "", { "dependencies": { "@prisma/config": "7.4.1", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.1", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-gDKOXwnPiMdB+uYMhMeN8jj4K7Cu3Q2wB/wUsITOoOk446HtVb8T9BZxFJ1Zop6alc89k6PMNdR2FZCpbXp/jw=="], + "prisma": ["prisma@7.4.2", "", { "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA=="], "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], @@ -330,6 +359,20 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "turbo": ["turbo@2.8.14", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.14", "turbo-darwin-arm64": "2.8.14", "turbo-linux-64": "2.8.14", "turbo-linux-arm64": "2.8.14", "turbo-windows-64": "2.8.14", "turbo-windows-arm64": "2.8.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.8.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ=="], + + "turbo-linux-64": ["turbo-linux-64@2.8.14", "", { "os": "linux", "cpu": "x64" }, "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg=="], + + "turbo-windows-64": ["turbo-windows-64@2.8.14", "", { "os": "win32", "cpu": "x64" }, "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], @@ -346,9 +389,9 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1" } }, "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.1", "", { "dependencies": { "@prisma/debug": "7.4.1" } }, "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg=="], + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], diff --git a/package.json b/package.json index 8ed3a01..2a72244 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,20 @@ { - "name": "@project-cvsa/backend", - "module": "index.ts", - "type": "module", - "version": "0.0.1", + "name": "cvsa", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], "scripts": { - "format": "bunx --bun biome format --write", - "type": "bunx --bun tsc --noEmit", - "lint": "bunx --bun biome lint", - "dev": "bun --hot src/index.ts", - "start": "bun src/index.ts", - "test": "bun test", - "test:coverage": "bun test --coverage", - "test:unit": "bun test tests/unit", - "test:e2e": "bun test tests/e2e", - "test:integration": "bun test tests/integration", - "db-gen": "bunx --bun prisma generate", - "migrate": "bunx --bun prisma migrate dev --create-only", - "migrate:dev": "bunx --bun prisma migrate dev", - "db-sync": "bunx --bun prisma db push", - "test:db:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", - "db:studio": "bunx --bun prisma studio" + "dev": "bunx --bun turbo run dev", + "build": "bunx --bun turbo run build", + "lint": "biome check .", + "lint:fix": "biome check --write --unsafe .", + "format": "biome format --write .", + "test": "bunx --bun turbo run test" }, "devDependencies": { - "@biomejs/biome": "^2.4.4", - "@types/bun": "latest", - "prisma": "^7.4.1", - "prisma-json-types-generator": "^4.1.1" + "turbo": "latest" }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@elysiajs/eden": "^1.0.5", - "@prisma/adapter-pg": "^7.4.1", - "@prisma/client": "^7.4.1", - "@t3-oss/env-core": "^0.13.10", - "@types/pg": "^8.16.0", - "argon2": "^0.44.0", - "chalk": "^5.6.2", - "elysia": "^1.4.26", - "elysia-ip": "^1.0.10", - "elysia-rate-limit": "^4.5.0", - "nanoid": "^5.1.6", - "zod": "^4.3.6" - } + "packageManager": "bun@1.3.10" } diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..69ae86c --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@project-cvsa/configs", + "exports": { + "./tsconfig/*": "./tsconfig/*.json" + } +} diff --git a/packages/config/tsconfig/base.json b/packages/config/tsconfig/base.json new file mode 100644 index 0000000..00d78f8 --- /dev/null +++ b/packages/config/tsconfig/base.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "incremental": true, + "resolveJsonModule": true + } +} diff --git a/packages/config/tsconfig/dom.json b/packages/config/tsconfig/dom.json new file mode 100644 index 0000000..4fa7758 --- /dev/null +++ b/packages/config/tsconfig/dom.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "jsx": "preserve", + "moduleResolution": "bundler" + } +} diff --git a/packages/config/tsconfig/server.json b/packages/config/tsconfig/server.json new file mode 100644 index 0000000..aaa48a4 --- /dev/null +++ b/packages/config/tsconfig/server.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext"], + "module": "preserve", + "allowImportingTsExtensions": true, + "noEmit": true + } +} diff --git a/packages/db/.env.example b/packages/db/.env.example new file mode 100644 index 0000000..701c7b7 --- /dev/null +++ b/packages/db/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file diff --git a/packages/db/.gitignore b/packages/db/.gitignore new file mode 100644 index 0000000..b50a6d8 --- /dev/null +++ b/packages/db/.gitignore @@ -0,0 +1 @@ +prisma/generated diff --git a/packages/db/README.md b/packages/db/README.md new file mode 100644 index 0000000..5c6b150 --- /dev/null +++ b/packages/db/README.md @@ -0,0 +1,11 @@ +# @project-cvsa/db + +## Available Scripts + +- `gen` – Generate Prisma client and schema artifacts +- `deploy` – Deploy database changes +- `migrate` – Create a new migration file without applying it +- `migrate:dev` – Apply migrations to the development database +- `push` – Push schema changes directly to the database +- `test:setup` – Reset and apply migrations for test environment +- `studio` – Open Prisma Studio database GUI diff --git a/packages/db/index.ts b/packages/db/index.ts new file mode 100644 index 0000000..c792826 --- /dev/null +++ b/packages/db/index.ts @@ -0,0 +1,3 @@ +export * from "./prisma/generated/client"; +export * from "./prisma/generated/models"; +export * from "./prisma/generated/enums"; diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..8c06bcc --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,26 @@ +{ + "name": "@project-cvsa/db", + "type": "module", + "module": "index.ts", + "scripts": { + "gen": "bunx --bun prisma generate", + "deploy": "", + "migrate": "bunx --bun prisma migrate dev --create-only", + "migrate:dev": "bunx --bun prisma migrate dev", + "push": "bunx --bun prisma db push", + "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", + "studio": "bunx --bun prisma studio" + }, + "devDependencies": { + "prisma": "^7.4.1", + "prisma-json-types-generator": "^4.1.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.4.1", + "@prisma/client": "^7.4.1", + "@types/pg": "^8.16.0" + } +} diff --git a/prisma.config.ts b/packages/db/prisma.config.ts similarity index 100% rename from prisma.config.ts rename to packages/db/prisma.config.ts diff --git a/prisma/migrations/20260227101459_init/migration.sql b/packages/db/prisma/migrations/20260227101459_init/migration.sql similarity index 100% rename from prisma/migrations/20260227101459_init/migration.sql rename to packages/db/prisma/migrations/20260227101459_init/migration.sql diff --git a/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml similarity index 100% rename from prisma/migrations/migration_lock.toml rename to packages/db/prisma/migrations/migration_lock.toml diff --git a/prisma/models/auth/README.md b/packages/db/prisma/models/auth/README.md similarity index 100% rename from prisma/models/auth/README.md rename to packages/db/prisma/models/auth/README.md diff --git a/prisma/models/auth/session.prisma b/packages/db/prisma/models/auth/session.prisma similarity index 100% rename from prisma/models/auth/session.prisma rename to packages/db/prisma/models/auth/session.prisma diff --git a/prisma/models/auth/user.prisma b/packages/db/prisma/models/auth/user.prisma similarity index 100% rename from prisma/models/auth/user.prisma rename to packages/db/prisma/models/auth/user.prisma diff --git a/prisma/models/core/README.md b/packages/db/prisma/models/core/README.md similarity index 100% rename from prisma/models/core/README.md rename to packages/db/prisma/models/core/README.md diff --git a/prisma/models/core/album.prisma b/packages/db/prisma/models/core/album.prisma similarity index 100% rename from prisma/models/core/album.prisma rename to packages/db/prisma/models/core/album.prisma diff --git a/prisma/models/core/artist.prisma b/packages/db/prisma/models/core/artist.prisma similarity index 100% rename from prisma/models/core/artist.prisma rename to packages/db/prisma/models/core/artist.prisma diff --git a/prisma/models/core/artistRole.prisma b/packages/db/prisma/models/core/artistRole.prisma similarity index 100% rename from prisma/models/core/artistRole.prisma rename to packages/db/prisma/models/core/artistRole.prisma diff --git a/prisma/models/core/externalLink.prisma b/packages/db/prisma/models/core/externalLink.prisma similarity index 100% rename from prisma/models/core/externalLink.prisma rename to packages/db/prisma/models/core/externalLink.prisma diff --git a/prisma/models/core/group.prisma b/packages/db/prisma/models/core/group.prisma similarity index 100% rename from prisma/models/core/group.prisma rename to packages/db/prisma/models/core/group.prisma diff --git a/prisma/models/core/lyrics.prisma b/packages/db/prisma/models/core/lyrics.prisma similarity index 100% rename from prisma/models/core/lyrics.prisma rename to packages/db/prisma/models/core/lyrics.prisma diff --git a/prisma/models/core/singer.prisma b/packages/db/prisma/models/core/singer.prisma similarity index 100% rename from prisma/models/core/singer.prisma rename to packages/db/prisma/models/core/singer.prisma diff --git a/prisma/models/core/song.prisma b/packages/db/prisma/models/core/song.prisma similarity index 100% rename from prisma/models/core/song.prisma rename to packages/db/prisma/models/core/song.prisma diff --git a/prisma/models/core/songSeries.prisma b/packages/db/prisma/models/core/songSeries.prisma similarity index 100% rename from prisma/models/core/songSeries.prisma rename to packages/db/prisma/models/core/songSeries.prisma diff --git a/prisma/models/core/svsEngine.prisma b/packages/db/prisma/models/core/svsEngine.prisma similarity index 100% rename from prisma/models/core/svsEngine.prisma rename to packages/db/prisma/models/core/svsEngine.prisma diff --git a/prisma/models/core/svsEngineVersion.prisma b/packages/db/prisma/models/core/svsEngineVersion.prisma similarity index 100% rename from prisma/models/core/svsEngineVersion.prisma rename to packages/db/prisma/models/core/svsEngineVersion.prisma diff --git a/prisma/models/core/tag.prisma b/packages/db/prisma/models/core/tag.prisma similarity index 100% rename from prisma/models/core/tag.prisma rename to packages/db/prisma/models/core/tag.prisma diff --git a/prisma/models/core/voicebank.prisma b/packages/db/prisma/models/core/voicebank.prisma similarity index 100% rename from prisma/models/core/voicebank.prisma rename to packages/db/prisma/models/core/voicebank.prisma diff --git a/prisma/models/meta/README.md b/packages/db/prisma/models/meta/README.md similarity index 100% rename from prisma/models/meta/README.md rename to packages/db/prisma/models/meta/README.md diff --git a/prisma/models/meta/history.prisma b/packages/db/prisma/models/meta/history.prisma similarity index 100% rename from prisma/models/meta/history.prisma rename to packages/db/prisma/models/meta/history.prisma diff --git a/prisma/models/meta/permission.prisma b/packages/db/prisma/models/meta/permission.prisma similarity index 100% rename from prisma/models/meta/permission.prisma rename to packages/db/prisma/models/meta/permission.prisma diff --git a/prisma/models/meta/role.prisma b/packages/db/prisma/models/meta/role.prisma similarity index 100% rename from prisma/models/meta/role.prisma rename to packages/db/prisma/models/meta/role.prisma diff --git a/prisma/models/models.prisma b/packages/db/prisma/models/models.prisma similarity index 100% rename from prisma/models/models.prisma rename to packages/db/prisma/models/models.prisma diff --git a/prisma/models/platform/README.md b/packages/db/prisma/models/platform/README.md similarity index 100% rename from prisma/models/platform/README.md rename to packages/db/prisma/models/platform/README.md diff --git a/prisma/models/platform/file.prisma b/packages/db/prisma/models/platform/file.prisma similarity index 100% rename from prisma/models/platform/file.prisma rename to packages/db/prisma/models/platform/file.prisma diff --git a/prisma/models/platform/post.prisma b/packages/db/prisma/models/platform/post.prisma similarity index 100% rename from prisma/models/platform/post.prisma rename to packages/db/prisma/models/platform/post.prisma diff --git a/prisma/models/platform/reputationHistory.prisma b/packages/db/prisma/models/platform/reputationHistory.prisma similarity index 100% rename from prisma/models/platform/reputationHistory.prisma rename to packages/db/prisma/models/platform/reputationHistory.prisma diff --git a/prisma/relations/README.md b/packages/db/prisma/relations/README.md similarity index 100% rename from prisma/relations/README.md rename to packages/db/prisma/relations/README.md diff --git a/prisma/relations/core/singerOfSong.prisma b/packages/db/prisma/relations/core/singerOfSong.prisma similarity index 100% rename from prisma/relations/core/singerOfSong.prisma rename to packages/db/prisma/relations/core/singerOfSong.prisma diff --git a/prisma/relations/core/singerSvsEngine.prisma b/packages/db/prisma/relations/core/singerSvsEngine.prisma similarity index 100% rename from prisma/relations/core/singerSvsEngine.prisma rename to packages/db/prisma/relations/core/singerSvsEngine.prisma diff --git a/prisma/relations/core/singerSvsEngineVersion.prisma b/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma similarity index 100% rename from prisma/relations/core/singerSvsEngineVersion.prisma rename to packages/db/prisma/relations/core/singerSvsEngineVersion.prisma diff --git a/prisma/relations/core/songInAlbum.prisma b/packages/db/prisma/relations/core/songInAlbum.prisma similarity index 100% rename from prisma/relations/core/songInAlbum.prisma rename to packages/db/prisma/relations/core/songInAlbum.prisma diff --git a/prisma/relations/core/songInSeries.prisma b/packages/db/prisma/relations/core/songInSeries.prisma similarity index 100% rename from prisma/relations/core/songInSeries.prisma rename to packages/db/prisma/relations/core/songInSeries.prisma diff --git a/prisma/relations/core/songTag.prisma b/packages/db/prisma/relations/core/songTag.prisma similarity index 100% rename from prisma/relations/core/songTag.prisma rename to packages/db/prisma/relations/core/songTag.prisma diff --git a/prisma/relations/meta/rolePermission.prisma b/packages/db/prisma/relations/meta/rolePermission.prisma similarity index 100% rename from prisma/relations/meta/rolePermission.prisma rename to packages/db/prisma/relations/meta/rolePermission.prisma diff --git a/prisma/relations/relations.prisma b/packages/db/prisma/relations/relations.prisma similarity index 100% rename from prisma/relations/relations.prisma rename to packages/db/prisma/relations/relations.prisma diff --git a/prisma/schema.prisma b/packages/db/prisma/schema.prisma similarity index 100% rename from prisma/schema.prisma rename to packages/db/prisma/schema.prisma diff --git a/prisma/types.ts b/packages/db/prisma/types.ts similarity index 100% rename from prisma/types.ts rename to packages/db/prisma/types.ts diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 9ccda83..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "@prisma/*": ["./prisma/*"], - "@routes/*": ["./src/routes/*"], - "@lib/*": ["./src/lib/*"], - "@repositories/*": ["./src/repositories/*"], - "@services/*": ["./src/services/*"], - "@controllers/*": ["./src/controllers/*"], - "@schemas/*": ["./src/schemas/*"], - "@plugins/*": ["./src/plugins/*"], - "@/*": ["./src/*"] - }, - - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..2f8df2c --- /dev/null +++ b/turbo.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".output/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "test": {} + } +} From ad18386e599f04c145ffefd0c675790a2d330af8 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Thu, 12 Mar 2026 01:30:11 +0800 Subject: [PATCH 03/37] refactor(auth): use Better Auth framework for auth (#8) --- .vscode/settings.json | 18 ++ apps/backend/package.json | 8 +- apps/backend/src/{lib => common}/env.ts | 0 .../index.ts => common/error/AppError.ts} | 0 .../error/ConflictError.ts} | 2 +- .../error/RateLimitError.ts} | 2 +- apps/backend/src/common/error/index.ts | 3 + apps/backend/src/common/middlewares/auth.ts | 35 ++++ apps/backend/src/common/middlewares/index.ts | 1 + apps/backend/src/{lib => common}/prisma.ts | 2 +- .../schemas}/ErrorResponseSchema.ts | 0 .../common => common/schemas}/index.ts | 0 apps/backend/src/common/utils/index.ts | 1 + apps/backend/src/common/utils/randomId.ts | 4 + apps/backend/src/containers.ts | 9 - apps/backend/src/errorHandler.ts | 59 ++++++ apps/backend/src/index.ts | 46 +---- apps/backend/src/modules/auth/index.ts | 1 + apps/backend/src/modules/auth/lib/index.ts | 47 +++++ .../src/modules/auth/routes/currentUser.ts | 31 ++++ .../auth => modules/auth/routes}/index.ts | 0 .../backend/src/modules/auth/routes/signup.ts | 64 +++++++ .../auth/schema}/currentUser.ts | 2 + .../auth => modules/auth/schema}/index.ts | 0 .../auth => modules/auth/schema}/signup.ts | 2 +- apps/backend/src/modules/index.ts | 1 + .../src/repositories/SessionRepository.ts | 64 ------- .../src/repositories/UserRepository.ts | 54 ------ apps/backend/src/repositories/index.ts | 4 - .../interfaces/ISessionRepository.ts | 23 --- .../interfaces/IUserRepository.ts | 16 -- .../src/repositories/interfaces/index.ts | 6 - apps/backend/src/routes/auth/currentUser.ts | 48 ----- apps/backend/src/routes/auth/signup.ts | 50 ----- apps/backend/src/services/AuthService.ts | 173 ------------------ apps/backend/src/services/index.ts | 2 - .../src/services/interfaces/IAuthService.ts | 18 -- apps/backend/src/services/interfaces/index.ts | 1 - apps/backend/src/startMessage.ts | 2 +- apps/backend/tests/e2e/auth.test.ts | 84 +++++---- .../repositories/SessionRepository.test.ts | 92 ---------- .../repositories/UserRepository.test.ts | 98 ---------- .../tests/unit/lib/conflictError.test.ts | 21 --- .../tests/unit/services/AuthService.test.ts | 166 ----------------- apps/backend/tsconfig.json | 23 ++- bun.lock | 64 +++++++ packages/config/package.json | 6 - packages/config/tsconfig/base.json | 12 -- packages/config/tsconfig/dom.json | 11 -- packages/config/tsconfig/server.json | 11 -- .../20260310103700_better_auth/migration.sql | 80 ++++++++ packages/db/prisma/models/auth/account.prisma | 20 ++ packages/db/prisma/models/auth/session.prisma | 19 +- packages/db/prisma/models/auth/user.prisma | 8 +- .../db/prisma/models/auth/verification.prisma | 12 ++ 55 files changed, 538 insertions(+), 988 deletions(-) create mode 100644 .vscode/settings.json rename apps/backend/src/{lib => common}/env.ts (100%) rename apps/backend/src/{lib/error/index.ts => common/error/AppError.ts} (100%) rename apps/backend/src/{lib/error/conflict.ts => common/error/ConflictError.ts} (85%) rename apps/backend/src/{lib/error/rateLimit.ts => common/error/RateLimitError.ts} (78%) create mode 100644 apps/backend/src/common/error/index.ts create mode 100644 apps/backend/src/common/middlewares/auth.ts create mode 100644 apps/backend/src/common/middlewares/index.ts rename apps/backend/src/{lib => common}/prisma.ts (86%) rename apps/backend/src/{schemas/common => common/schemas}/ErrorResponseSchema.ts (100%) rename apps/backend/src/{schemas/common => common/schemas}/index.ts (100%) create mode 100644 apps/backend/src/common/utils/index.ts create mode 100644 apps/backend/src/common/utils/randomId.ts delete mode 100644 apps/backend/src/containers.ts create mode 100644 apps/backend/src/errorHandler.ts create mode 100644 apps/backend/src/modules/auth/index.ts create mode 100644 apps/backend/src/modules/auth/lib/index.ts create mode 100644 apps/backend/src/modules/auth/routes/currentUser.ts rename apps/backend/src/{routes/auth => modules/auth/routes}/index.ts (100%) create mode 100644 apps/backend/src/modules/auth/routes/signup.ts rename apps/backend/src/{schemas/auth => modules/auth/schema}/currentUser.ts (76%) rename apps/backend/src/{schemas/auth => modules/auth/schema}/index.ts (100%) rename apps/backend/src/{schemas/auth => modules/auth/schema}/signup.ts (89%) create mode 100644 apps/backend/src/modules/index.ts delete mode 100644 apps/backend/src/repositories/SessionRepository.ts delete mode 100644 apps/backend/src/repositories/UserRepository.ts delete mode 100644 apps/backend/src/repositories/index.ts delete mode 100644 apps/backend/src/repositories/interfaces/ISessionRepository.ts delete mode 100644 apps/backend/src/repositories/interfaces/IUserRepository.ts delete mode 100644 apps/backend/src/repositories/interfaces/index.ts delete mode 100644 apps/backend/src/routes/auth/currentUser.ts delete mode 100644 apps/backend/src/routes/auth/signup.ts delete mode 100644 apps/backend/src/services/AuthService.ts delete mode 100644 apps/backend/src/services/index.ts delete mode 100644 apps/backend/src/services/interfaces/IAuthService.ts delete mode 100644 apps/backend/src/services/interfaces/index.ts delete mode 100644 apps/backend/tests/integration/repositories/SessionRepository.test.ts delete mode 100644 apps/backend/tests/integration/repositories/UserRepository.test.ts delete mode 100644 apps/backend/tests/unit/lib/conflictError.test.ts delete mode 100644 apps/backend/tests/unit/services/AuthService.test.ts delete mode 100644 packages/config/package.json delete mode 100644 packages/config/tsconfig/base.json delete mode 100644 packages/config/tsconfig/dom.json delete mode 100644 packages/config/tsconfig/server.json create mode 100644 packages/db/prisma/migrations/20260310103700_better_auth/migration.sql create mode 100644 packages/db/prisma/models/auth/account.prisma create mode 100644 packages/db/prisma/models/auth/verification.prisma diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..81fc238 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + } +} diff --git a/apps/backend/package.json b/apps/backend/package.json index 53de1ae..d93dad2 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -20,17 +20,19 @@ "typescript": "^5" }, "dependencies": { + "@better-auth/prisma-adapter": "^1.5.4", "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", + "@project-cvsa/configs": "workspace:*", + "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", + "better-auth": "^1.5.4", "chalk": "^5.6.2", "elysia": "^1.4.26", "elysia-ip": "^1.0.10", "elysia-rate-limit": "^4.5.0", "nanoid": "^5.1.6", - "zod": "^4.3.6", - "@project-cvsa/db": "workspace:*", - "@project-cvsa/configs": "workspace:*" + "zod": "^4.3.6" } } diff --git a/apps/backend/src/lib/env.ts b/apps/backend/src/common/env.ts similarity index 100% rename from apps/backend/src/lib/env.ts rename to apps/backend/src/common/env.ts diff --git a/apps/backend/src/lib/error/index.ts b/apps/backend/src/common/error/AppError.ts similarity index 100% rename from apps/backend/src/lib/error/index.ts rename to apps/backend/src/common/error/AppError.ts diff --git a/apps/backend/src/lib/error/conflict.ts b/apps/backend/src/common/error/ConflictError.ts similarity index 85% rename from apps/backend/src/lib/error/conflict.ts rename to apps/backend/src/common/error/ConflictError.ts index 88dd464..a81c370 100644 --- a/apps/backend/src/lib/error/conflict.ts +++ b/apps/backend/src/common/error/ConflictError.ts @@ -1,4 +1,4 @@ -import { AppError } from "@lib/error"; +import { AppError } from "@common/error"; export class ConflictError extends AppError { constructor( diff --git a/apps/backend/src/lib/error/rateLimit.ts b/apps/backend/src/common/error/RateLimitError.ts similarity index 78% rename from apps/backend/src/lib/error/rateLimit.ts rename to apps/backend/src/common/error/RateLimitError.ts index ebb1401..6ea6e7a 100644 --- a/apps/backend/src/lib/error/rateLimit.ts +++ b/apps/backend/src/common/error/RateLimitError.ts @@ -1,4 +1,4 @@ -import { AppError } from "."; +import { AppError } from "@common/error"; export class RateLimitError extends AppError { constructor(message = "Request rate limit exceeded") { diff --git a/apps/backend/src/common/error/index.ts b/apps/backend/src/common/error/index.ts new file mode 100644 index 0000000..4c04789 --- /dev/null +++ b/apps/backend/src/common/error/index.ts @@ -0,0 +1,3 @@ +export * from "./AppError"; +export * from "./ConflictError"; +export * from "./RateLimitError"; diff --git a/apps/backend/src/common/middlewares/auth.ts b/apps/backend/src/common/middlewares/auth.ts new file mode 100644 index 0000000..c0a9404 --- /dev/null +++ b/apps/backend/src/common/middlewares/auth.ts @@ -0,0 +1,35 @@ +import { AppError } from "@common/error"; +import { auth } from "@modules/auth/lib"; +import { Elysia } from "elysia"; +import type { User } from "@project-cvsa/db"; + +type BetterAuthUser = Exclude>, null>["user"]; + +const convertUser = (user: BetterAuthUser): User => { + const { image, roleId } = user; + return { + ...user, + image: image || null, + roleId: roleId || null, + username: user.username || "", + displayUsername: user.displayUsername || "", + }; +}; + +export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( + { as: "scoped" }, + async ({ headers }) => { + const session = await auth.api.getSession({ + headers: headers, + }); + + if (!session) { + throw new AppError("Unauthorized", "UNAUTHORIZED", 401); + } + + return { + session, + user: convertUser(session.user) as User, + }; + } +); diff --git a/apps/backend/src/common/middlewares/index.ts b/apps/backend/src/common/middlewares/index.ts new file mode 100644 index 0000000..97ccf76 --- /dev/null +++ b/apps/backend/src/common/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./auth"; diff --git a/apps/backend/src/lib/prisma.ts b/apps/backend/src/common/prisma.ts similarity index 86% rename from apps/backend/src/lib/prisma.ts rename to apps/backend/src/common/prisma.ts index c231dc1..562cfe0 100644 --- a/apps/backend/src/lib/prisma.ts +++ b/apps/backend/src/common/prisma.ts @@ -1,6 +1,6 @@ import { PrismaClient } from "@project-cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "@lib/env"; +import { env } from "@common/env"; const adapter = new PrismaPg({ connectionString: env.DATABASE_URL, diff --git a/apps/backend/src/schemas/common/ErrorResponseSchema.ts b/apps/backend/src/common/schemas/ErrorResponseSchema.ts similarity index 100% rename from apps/backend/src/schemas/common/ErrorResponseSchema.ts rename to apps/backend/src/common/schemas/ErrorResponseSchema.ts diff --git a/apps/backend/src/schemas/common/index.ts b/apps/backend/src/common/schemas/index.ts similarity index 100% rename from apps/backend/src/schemas/common/index.ts rename to apps/backend/src/common/schemas/index.ts diff --git a/apps/backend/src/common/utils/index.ts b/apps/backend/src/common/utils/index.ts new file mode 100644 index 0000000..75f9eaa --- /dev/null +++ b/apps/backend/src/common/utils/index.ts @@ -0,0 +1 @@ +export * from "./randomId"; diff --git a/apps/backend/src/common/utils/randomId.ts b/apps/backend/src/common/utils/randomId.ts new file mode 100644 index 0000000..ed76be3 --- /dev/null +++ b/apps/backend/src/common/utils/randomId.ts @@ -0,0 +1,4 @@ +import { customAlphabet } from "nanoid"; +const alphabet = "123456789ABCDEFGHJKMNPQRSTWXYZ"; +const fn = customAlphabet(alphabet); +export const getRandomId = (length: number) => fn(length); diff --git a/apps/backend/src/containers.ts b/apps/backend/src/containers.ts deleted file mode 100644 index 47c5f32..0000000 --- a/apps/backend/src/containers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prisma } from "@lib/prisma"; -import { createUserRepository } from "@repositories/UserRepository"; -import { createSessionRepository } from "@repositories/SessionRepository"; -import { createAuthService } from "@services/AuthService"; - -const userRepo = createUserRepository(prisma); -const sessionRepo = createSessionRepository(prisma); - -export const authService = createAuthService(userRepo, sessionRepo, prisma); diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts new file mode 100644 index 0000000..c197d24 --- /dev/null +++ b/apps/backend/src/errorHandler.ts @@ -0,0 +1,59 @@ +import { AppError } from "@common/error"; +import { APIError } from "better-auth"; +import type { ErrorHandler } from "elysia"; +import { ZodError } from "zod"; + +export const errorHandler: ErrorHandler<{ + readonly AppError: AppError; +}> = ({ code, status, error }) => { + if (code === "NOT_FOUND") + return status(404, { + message: "The requested resource was not found.", + }); + if (code === "VALIDATION") { + const detail = error.detail(error.message); + if (typeof detail === "string") { + return status(422, { + code: "VALIDATION_ERROR", + message: detail, + }); + } + return status(422, { + code: "VALIDATION_ERROR", + message: detail.summary, + }); + } + if (error instanceof AppError) { + return status(error.statusCode, { + code: error.code, + message: error.message, + }); + } + if (error instanceof ZodError) { + return status(422, { + code: "VALIDATION_ERROR", + message: error.message, + }); + } + if (error instanceof APIError) { + if ( + ["USERNAME_IS_ALREADY_TAKEN", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"].includes( + error.body?.code || "" + ) + ) { + return status(409, { + code: error.body?.code, + message: error.body?.message, + }); + } + return status(error.statusCode, { + code: error.body?.code, + message: error.body?.message, + }); + } + console.error(error); + return status(500, { + code: "SERVER_ERROR", + message: "Internal server error", + }); +}; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index fb3a3f7..6ae631b 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,52 +1,14 @@ -import { Elysia, type ErrorHandler } from "elysia"; +import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler } from "@routes/auth"; -import { AppError } from "@lib/error"; -import { ZodError } from "zod"; +import { authHandler } from "@modules/index"; +import { AppError } from "@common/error"; +import { errorHandler } from "./errorHandler"; const [host, port] = getBindingInfo(); logStartup(host, port); -const errorHandler: ErrorHandler<{ - readonly AppError: AppError; -}> = ({ code, status, error }) => { - if (code === "NOT_FOUND") - return status(404, { - message: "The requested resource was not found.", - }); - if (code === "VALIDATION") { - const detail = error.detail(error.message); - if (typeof detail === "string") { - return status(422, { - code: "VALIDATION_ERROR", - message: detail, - }); - } - return status(422, { - code: "VALIDATION_ERROR", - message: detail.summary, - }); - } - if (error instanceof AppError) { - return status(error.statusCode, { - code: error.code, - message: error.message, - }); - } - if (error instanceof ZodError) { - return status(422, { - code: "VALIDATION_ERROR", - message: error.message, - }); - } - return status(500, { - code: "SERVER_ERROR", - message: "Internal server error", - }); -}; - export const app = new Elysia({ serve: { hostname: host, diff --git a/apps/backend/src/modules/auth/index.ts b/apps/backend/src/modules/auth/index.ts new file mode 100644 index 0000000..9bf9b1b --- /dev/null +++ b/apps/backend/src/modules/auth/index.ts @@ -0,0 +1 @@ +export * from "./routes"; diff --git a/apps/backend/src/modules/auth/lib/index.ts b/apps/backend/src/modules/auth/lib/index.ts new file mode 100644 index 0000000..117eb92 --- /dev/null +++ b/apps/backend/src/modules/auth/lib/index.ts @@ -0,0 +1,47 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "@better-auth/prisma-adapter"; +import { prisma } from "@common/prisma"; +import { username } from "better-auth/plugins"; +import { getRandomId } from "@common/utils/randomId"; +import { bearer } from "better-auth/plugins"; + +const DAY = 24 * 60 * 60; +const HOUR = 60 * 60; + +export const auth = betterAuth({ + database: prismaAdapter(prisma, { provider: "postgresql" }), + user: { + additionalFields: { + reputation: { type: "number", defaultValue: 0 }, + roleId: { type: "number", required: false }, + }, + }, + password: { + hash: (password: string) => + Bun.password.hash(password, { + algorithm: "argon2id", + memoryCost: 4, + }), + verify: (password: string, hash: string) => Bun.password.verify(password, hash), + }, + session: { + expiresIn: 90 * DAY, + updateAge: 1 * DAY, + freshAge: 3 * HOUR, + deferSessionRefresh: true, + }, + advanced: { + database: { + generateId: (options) => { + if (options.model === "user" || options.model === "users") { + return getRandomId(7); + } + return getRandomId(12); + }, + }, + }, + emailAndPassword: { + enabled: true, + }, + plugins: [username(), bearer()], +}); diff --git a/apps/backend/src/modules/auth/routes/currentUser.ts b/apps/backend/src/modules/auth/routes/currentUser.ts new file mode 100644 index 0000000..60281d0 --- /dev/null +++ b/apps/backend/src/modules/auth/routes/currentUser.ts @@ -0,0 +1,31 @@ +import { Elysia } from "elysia"; +import { GetCurrentUserResponseSchema } from "@modules/auth/schema"; +import { ErrorResponseSchema } from "@common/schemas"; +import { authMiddleware } from "@common/middlewares"; + +export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( + "/me", + async ({ status, user }) => { + return status(200, { + id: user.id, + username: user.username || "", + displayName: user.name, + email: user.email, + createdAt: user.createdAt, + image: user.image, + }); + }, + { + detail: { + summary: "Get current user", + description: "", + }, + response: { + 200: GetCurrentUserResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 422: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/routes/auth/index.ts b/apps/backend/src/modules/auth/routes/index.ts similarity index 100% rename from apps/backend/src/routes/auth/index.ts rename to apps/backend/src/modules/auth/routes/index.ts diff --git a/apps/backend/src/modules/auth/routes/signup.ts b/apps/backend/src/modules/auth/routes/signup.ts new file mode 100644 index 0000000..233feda --- /dev/null +++ b/apps/backend/src/modules/auth/routes/signup.ts @@ -0,0 +1,64 @@ +import { Elysia } from "elysia"; +import { ip } from "elysia-ip"; +import { rateLimit } from "elysia-rate-limit"; +import { SignupRequestSchema, SignupResponseSchema } from "@modules/auth/schema"; +import { ErrorResponseSchema } from "@common/schemas"; +import { RateLimitError, AppError } from "@common/error"; +import { auth } from "@modules/auth/lib"; +import { getRandomId } from "@common/utils"; + +export const signupHandler = new Elysia() + .use(ip()) + .use( + rateLimit({ + scoping: "global", + max: 50, + duration: 5 * 60 * 1000, + generator: () => "", // global limit + errorResponse: new RateLimitError(), + }) + ) + .post( + "/user", + async ({ body, status, headers }) => { + const { user, token } = await auth.api.signUpEmail({ + body: { + name: body.displayName || body.username, + email: + body.email || `delegate-${getRandomId(14).toLowerCase()}@projectcvsa.com`, + password: body.password, + username: body.username, + }, + headers: headers, + }); + if (!token) { + throw new AppError("Cannot create user", "INTERNAL_SERVER_ERROR", 500, { + cause: "Better Auth responded with no token", + }); + } + + return status(200, { + message: "Successfully registered", + data: { + id: user.id, + username: user.username || "", + displayName: user.name, + email: user.email, + token, + }, + }); + }, + { + body: SignupRequestSchema, + detail: { + summary: "User Registration", + description: "", + }, + response: { + 200: SignupResponseSchema, + 400: ErrorResponseSchema, + 422: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/schemas/auth/currentUser.ts b/apps/backend/src/modules/auth/schema/currentUser.ts similarity index 76% rename from apps/backend/src/schemas/auth/currentUser.ts rename to apps/backend/src/modules/auth/schema/currentUser.ts index 7c9b994..3f1b140 100644 --- a/apps/backend/src/schemas/auth/currentUser.ts +++ b/apps/backend/src/modules/auth/schema/currentUser.ts @@ -5,4 +5,6 @@ export const GetCurrentUserResponseSchema = z.object({ username: z.string(), displayName: z.string().optional().nullable(), email: z.email().optional().nullable(), + createdAt: z.date(), + image: z.string().optional().nullable(), }); diff --git a/apps/backend/src/schemas/auth/index.ts b/apps/backend/src/modules/auth/schema/index.ts similarity index 100% rename from apps/backend/src/schemas/auth/index.ts rename to apps/backend/src/modules/auth/schema/index.ts diff --git a/apps/backend/src/schemas/auth/signup.ts b/apps/backend/src/modules/auth/schema/signup.ts similarity index 89% rename from apps/backend/src/schemas/auth/signup.ts rename to apps/backend/src/modules/auth/schema/signup.ts index 6ab3cf0..cf25fec 100644 --- a/apps/backend/src/schemas/auth/signup.ts +++ b/apps/backend/src/modules/auth/schema/signup.ts @@ -7,7 +7,7 @@ export const SignupRequestSchema = z.object({ email: z.email().optional().nullable(), }); -export const SignupResponse200Schema = z.object({ +export const SignupResponseSchema = z.object({ message: z.string(), data: z.object({ id: z.string(), diff --git a/apps/backend/src/modules/index.ts b/apps/backend/src/modules/index.ts new file mode 100644 index 0000000..97ccf76 --- /dev/null +++ b/apps/backend/src/modules/index.ts @@ -0,0 +1 @@ +export * from "./auth"; diff --git a/apps/backend/src/repositories/SessionRepository.ts b/apps/backend/src/repositories/SessionRepository.ts deleted file mode 100644 index 9257ea5..0000000 --- a/apps/backend/src/repositories/SessionRepository.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { prisma } from "@lib/prisma"; -import type { ISessionRepository, CreateSessionData, SessionWithSecret } from "./interfaces"; -import type { Session, Prisma, PrismaClient } from "@project-cvsa/db"; -import crypto from "node:crypto"; - -export class SessionRepository implements ISessionRepository { - private prisma: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.prisma = prismaClient; - } - - async create( - data: CreateSessionData, - transaction?: Prisma.TransactionClient - ): Promise { - const sha256Hasher = new Bun.CryptoHasher("sha256"); - // 120 bits entropy - const id = crypto.randomBytes(15).toString("hex"); - const secret = crypto.randomBytes(15).toString("hex"); - sha256Hasher.update(secret); - const secretHash = sha256Hasher.digest("hex"); - - const db = transaction ?? this.prisma; - const session = await db.session.create({ - data: { - id, - userId: data.userId, - secretHash: secretHash, - userAgent: data.userAgent, - ipAddress: data.ipAddress, - }, - }); - - // Return session with the plain secret for token generation - return Object.assign(session, { secret }); - } - - async findById(id: string, transaction?: Prisma.TransactionClient): Promise { - const db = transaction ?? this.prisma; - return await db.session.findUnique({ - where: { id }, - }); - } - - async findByIdAndSecretHash( - id: string, - secretHash: string, - transaction?: Prisma.TransactionClient - ): Promise { - const db = transaction ?? this.prisma; - return await db.session.findFirst({ - where: { - id, - secretHash, - }, - }); - } -} - -// Factory function to create SessionRepository with optional PrismaClient -export function createSessionRepository(prismaClient?: PrismaClient): SessionRepository { - return new SessionRepository(prismaClient); -} diff --git a/apps/backend/src/repositories/UserRepository.ts b/apps/backend/src/repositories/UserRepository.ts deleted file mode 100644 index 9ef419e..0000000 --- a/apps/backend/src/repositories/UserRepository.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { prisma } from "@lib/prisma"; -import type { IUserRepository, CreateUserData } from "./interfaces"; -import type { User } from "@project-cvsa/db"; -import type { Prisma, PrismaClient } from "@project-cvsa/db"; - -export class UserRepository implements IUserRepository { - private prisma: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.prisma = prismaClient; - } - - async create(data: CreateUserData, transaction?: Prisma.TransactionClient): Promise { - const db = transaction ?? this.prisma; - return await db.user.create({ - data: { - id: data.id, - username: data.username, - password: data.password, - displayName: data.displayName, - email: data.email, - }, - }); - } - - async findById(id: string, transaction?: Prisma.TransactionClient): Promise { - const db = transaction ?? this.prisma; - return await db.user.findUnique({ - where: { id }, - }); - } - - async findByUsername( - username: string, - transaction?: Prisma.TransactionClient - ): Promise { - const db = transaction ?? this.prisma; - return await db.user.findUnique({ - where: { username }, - }); - } - - async findByEmail(email: string, transaction?: Prisma.TransactionClient): Promise { - const db = transaction ?? this.prisma; - return await db.user.findUnique({ - where: { email }, - }); - } -} - -// Factory function to create UserRepository with optional PrismaClient -export function createUserRepository(prismaClient?: PrismaClient): UserRepository { - return new UserRepository(prismaClient); -} diff --git a/apps/backend/src/repositories/index.ts b/apps/backend/src/repositories/index.ts deleted file mode 100644 index d5b81ad..0000000 --- a/apps/backend/src/repositories/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type { IUserRepository, CreateUserData } from "./interfaces/IUserRepository"; -export type { ISessionRepository, CreateSessionData } from "./interfaces/ISessionRepository"; -export { UserRepository, createUserRepository } from "./UserRepository"; -export { SessionRepository, createSessionRepository } from "./SessionRepository"; diff --git a/apps/backend/src/repositories/interfaces/ISessionRepository.ts b/apps/backend/src/repositories/interfaces/ISessionRepository.ts deleted file mode 100644 index f4c1b49..0000000 --- a/apps/backend/src/repositories/interfaces/ISessionRepository.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Session, Prisma } from "@project-cvsa/db"; - -export interface CreateSessionData { - userId: string; - ipAddress?: string; - userAgent?: string; -} - -// Session with secret for token generation -export type SessionWithSecret = Session & { secret: string }; - -export interface ISessionRepository { - create( - data: CreateSessionData, - transaction?: Prisma.TransactionClient - ): Promise; - findById(id: string, transaction?: Prisma.TransactionClient): Promise; - findByIdAndSecretHash( - id: string, - secretHash: string, - transaction?: Prisma.TransactionClient - ): Promise; -} diff --git a/apps/backend/src/repositories/interfaces/IUserRepository.ts b/apps/backend/src/repositories/interfaces/IUserRepository.ts deleted file mode 100644 index 1bd461a..0000000 --- a/apps/backend/src/repositories/interfaces/IUserRepository.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { User, Prisma } from "@project-cvsa/db"; - -export interface CreateUserData { - id: string; - username: string; - password: string; - displayName?: string | null; - email?: string | null; -} - -export interface IUserRepository { - create(data: CreateUserData, transaction?: Prisma.TransactionClient): Promise; - findById(id: string, transaction?: Prisma.TransactionClient): Promise; - findByUsername(username: string, transaction?: Prisma.TransactionClient): Promise; - findByEmail(email: string, transaction?: Prisma.TransactionClient): Promise; -} diff --git a/apps/backend/src/repositories/interfaces/index.ts b/apps/backend/src/repositories/interfaces/index.ts deleted file mode 100644 index 821c43b..0000000 --- a/apps/backend/src/repositories/interfaces/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type { IUserRepository, CreateUserData } from "./IUserRepository"; -export type { - ISessionRepository, - CreateSessionData, - SessionWithSecret, -} from "./ISessionRepository"; diff --git a/apps/backend/src/routes/auth/currentUser.ts b/apps/backend/src/routes/auth/currentUser.ts deleted file mode 100644 index c8b5fd6..0000000 --- a/apps/backend/src/routes/auth/currentUser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Elysia } from "elysia"; -import { GetCurrentUserResponseSchema } from "@schemas/auth"; -import { ErrorResponseSchema } from "@schemas/common"; -import z from "zod"; -import { authService } from "@/containers"; - -const DUMMY_TOKEN = "000000000000000000000000000000.000000000000000000000000000000"; -const DUMMY_BEARER_TOKEN = `Bearer ${DUMMY_TOKEN}`; - -const authSchema = z - .string() - // Length: Bearer (7) + hex(30) + dot(1) + hex(30) = 68 - .length(68) - .startsWith("Bearer ") - .transform((val) => val.slice(7)) - .refine((token) => /^[0-9a-fA-F]{30}\.[0-9a-fA-F]{30}$/.test(token)); - -export const getCurrentUserHandler = new Elysia().decorate("authService", authService).get( - "/me", - async ({ headers, status, authService }) => { - const auth = headers.authorization ?? DUMMY_BEARER_TOKEN; - const parsedToken = authSchema.safeParse(auth); - const token = parsedToken.success ? parsedToken.data : DUMMY_TOKEN; - const user = await authService.verifyToken(token); - - if (!parsedToken.success || !user) { - return status(401, { code: "UNAUTHORIZED", message: "Unauthorized" }); - } - return status(200, { - id: user.id, - username: user.username, - displayName: user.displayName, - email: user.email, - }); - }, - { - detail: { - summary: "Get current user", - description: "", - }, - response: { - 200: GetCurrentUserResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); diff --git a/apps/backend/src/routes/auth/signup.ts b/apps/backend/src/routes/auth/signup.ts deleted file mode 100644 index 94aaad6..0000000 --- a/apps/backend/src/routes/auth/signup.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Elysia } from "elysia"; -import { ip } from "elysia-ip"; -import { rateLimit } from "elysia-rate-limit"; -import { SignupRequestSchema, SignupResponse200Schema } from "@schemas/auth"; -import { ErrorResponseSchema } from "@schemas/common"; -import { RateLimitError } from "@lib/error/rateLimit"; -import { authService } from "@/containers"; - -export const signupHandler = new Elysia() - .decorate("authService", authService) - .use(ip()) - .use( - rateLimit({ - scoping: "global", - max: 50, - duration: 5 * 60 * 1000, - generator: () => "", // global limit - errorResponse: new RateLimitError(), - }) - ) - .post( - "/user", - async ({ body, status, ip, headers, authService }) => { - const userAgent = headers["user-agent"]; - const { user, token } = await authService.register(body, ip, userAgent); - - return status(200, { - message: "Successfully registered", - data: { - id: user.id, - username: user.username, - displayName: user.displayName, - email: user.email, - token, - }, - }); - }, - { - body: SignupRequestSchema, - detail: { - summary: "User Registration", - description: "", - }, - response: { - 200: SignupResponse200Schema, - 400: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/services/AuthService.ts b/apps/backend/src/services/AuthService.ts deleted file mode 100644 index 3949f8e..0000000 --- a/apps/backend/src/services/AuthService.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { IAuthService, CreateUserData, AuthResult } from "./interfaces"; -import type { IUserRepository } from "@repositories/interfaces/IUserRepository"; -import type { ISessionRepository } from "@repositories/interfaces/ISessionRepository"; -import { prisma } from "@lib/prisma"; -import { AppError } from "@lib/error"; -import { ConflictError } from "@lib/error/conflict"; -import { Prisma, type User, type PrismaClient } from "@project-cvsa/db"; -import { customAlphabet } from "nanoid"; -import crypto from "node:crypto"; - -const MAX_RETRIES = 5; -const alphabet = "123456789ACDEFGHJKLMNPQRSTUVWXYZ"; -const nanoid = customAlphabet(alphabet, 7); -const PASSWORD_HASH_COST = 4; -const SESSION_SECRET_HASH_ALGORITHM = "sha256"; -const DUMMY_SESSION_SECRET = "0".repeat(30); -const DUMMY_PASSWORD_HASH = await Bun.password.hash(DUMMY_SESSION_SECRET, { - algorithm: "argon2id", - memoryCost: PASSWORD_HASH_COST, -}); - -function hashSessionSecret(secret: string): string { - const sha256Hasher = new Bun.CryptoHasher(SESSION_SECRET_HASH_ALGORITHM); - sha256Hasher.update(secret); - return sha256Hasher.digest("hex"); -} - -function fixedLengthEqual(left: string, right: string): boolean { - const leftBuffer = Buffer.from(left); - const rightBuffer = Buffer.from(right); - - if (leftBuffer.length !== rightBuffer.length) { - return false; - } - - return crypto.timingSafeEqual(leftBuffer, rightBuffer); -} - -interface AuthServiceOptions { - passwordVerify?: typeof Bun.password.verify; -} - -export class AuthService implements IAuthService { - private userRepository: IUserRepository; - private sessionRepository: ISessionRepository; - private prisma: PrismaClient; - private passwordVerify: typeof Bun.password.verify; - - constructor( - userRepo: IUserRepository, - sessionRepo: ISessionRepository, - prismaClient: PrismaClient = prisma, - options: AuthServiceOptions = {} - ) { - this.userRepository = userRepo; - this.sessionRepository = sessionRepo; - this.prisma = prismaClient; - this.passwordVerify = options.passwordVerify ?? Bun.password.verify; - } - - async register( - data: CreateUserData, - ipAddress?: string, - userAgent?: string - ): Promise { - return await this.prisma.$transaction(async (tx) => { - const hashedPassword = await Bun.password.hash(data.password, { - algorithm: "argon2id", - memoryCost: PASSWORD_HASH_COST, - }); - - let attempts = 0; - let user: User | null = null; - - while (attempts < MAX_RETRIES) { - try { - user = await this.userRepository.create( - { - id: nanoid(), - username: data.username, - password: hashedPassword, - displayName: data.displayName, - email: data.email, - }, - tx - ); - break; - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") { - // Normalize duplicate failures to execute the same expensive work - // before surfacing whether the username/email already exists. - await this.passwordVerify(data.password, DUMMY_PASSWORD_HASH); - - // TODO: Prisma driver adapter P2002 workaround - // remove after prisma/prisma#28281 is fixed - // meta.target missing -> using undocumented driverAdapterError shape - const adapterError = e.meta?.driverAdapterError as - | { cause?: { constraint?: { fields?: string[] } } } - | undefined; - const fields = adapterError?.cause?.constraint?.fields; - - if (fields?.includes("username")) { - throw new ConflictError( - "username", - "USERNAME_DUPLICATED", - "User name duplicated" - ); - } - if (fields?.includes("email")) { - throw new ConflictError( - "email", - "EMAIL_DUPLICATED", - "This email has been registered" - ); - } - if (fields?.includes("id")) { - attempts++; - continue; - } - } - - throw e; - } - } - - if (!user) { - throw new AppError("Failed to register", "ID_GENERATION_FAILED"); - } - - const session = await this.sessionRepository.create( - { - userId: user.id, - ipAddress, - userAgent, - }, - tx - ); - const token = `${session.id}.${session.secret}`; - return { - user, - token, - }; - }); - } - - async verifyToken(token: string): Promise { - const [id, secret, ...rest] = token.split("."); - const hasValidShape = Boolean(id && secret) && rest.length === 0; - const sessionId = id ?? DUMMY_SESSION_SECRET; - const sessionSecret = secret ?? DUMMY_SESSION_SECRET; - const secretHash = hashSessionSecret(sessionSecret); - - const session = await this.sessionRepository.findById(sessionId); - const storedSecretHash = session?.secretHash ?? hashSessionSecret(DUMMY_SESSION_SECRET); - const hashesMatch = fixedLengthEqual(secretHash, storedSecretHash); - - if (!hasValidShape || !session || !hashesMatch) { - return null; - } - - const user = await this.userRepository.findById(session.userId); - return user; - } -} - -// Factory function to create AuthService -export function createAuthService( - userRepo: IUserRepository, - sessionRepo: ISessionRepository, - prismaClient?: PrismaClient -): AuthService { - return new AuthService(userRepo, sessionRepo, prismaClient); -} diff --git a/apps/backend/src/services/index.ts b/apps/backend/src/services/index.ts deleted file mode 100644 index ce4b42b..0000000 --- a/apps/backend/src/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { IAuthService, CreateUserData, AuthResult } from "./interfaces"; -export { AuthService, createAuthService } from "./AuthService"; diff --git a/apps/backend/src/services/interfaces/IAuthService.ts b/apps/backend/src/services/interfaces/IAuthService.ts deleted file mode 100644 index 70ece2f..0000000 --- a/apps/backend/src/services/interfaces/IAuthService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { User } from "@project-cvsa/db"; - -export interface CreateUserData { - username: string; - password: string; - displayName?: string | null; - email?: string | null; -} - -export interface AuthResult { - user: User; - token: string; -} - -export interface IAuthService { - register(data: CreateUserData, ipAddress?: string, userAgent?: string): Promise; - verifyToken(token: string): Promise; -} diff --git a/apps/backend/src/services/interfaces/index.ts b/apps/backend/src/services/interfaces/index.ts deleted file mode 100644 index 8b1b437..0000000 --- a/apps/backend/src/services/interfaces/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { IAuthService, CreateUserData, AuthResult } from "./IAuthService"; diff --git a/apps/backend/src/startMessage.ts b/apps/backend/src/startMessage.ts index 1aa2370..ea4443d 100644 --- a/apps/backend/src/startMessage.ts +++ b/apps/backend/src/startMessage.ts @@ -1,4 +1,4 @@ -import { env } from "@lib/env"; +import { env } from "@common/env"; import chalk from "chalk"; import os from "node:os"; diff --git a/apps/backend/tests/e2e/auth.test.ts b/apps/backend/tests/e2e/auth.test.ts index 54b023a..1977826 100644 --- a/apps/backend/tests/e2e/auth.test.ts +++ b/apps/backend/tests/e2e/auth.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@lib/prisma"; +import { prisma } from "@common/prisma"; const api = treaty(app); @@ -13,6 +13,8 @@ describe("Registration E2E Tests - POST /v2/user", () => { afterAll(async () => { await prisma.$disconnect(); + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); }); test("should register a new user", async () => { @@ -30,13 +32,24 @@ describe("Registration E2E Tests - POST /v2/user", () => { data: { id: expect.any(String), username: payload.username, - displayName: null, + displayName: expect.any(String), email: payload.email, token: expect.any(String), }, }); }); + test("should fail when nothing is provided", async () => { + // Create the first user + // @ts-expect-error it's intended + const { error, status } = await api.v2.user.post({}); + + expect(status).toBe(422); + expect(error?.value).toMatchObject({ + code: "VALIDATION_ERROR", + }); + }); + test("should fail when username already exists", async () => { const payload = { username: "duplicate_user", @@ -56,7 +69,30 @@ describe("Registration E2E Tests - POST /v2/user", () => { // Should return 409 Conflict expect(status).toBe(409); expect(error?.value).toMatchObject({ - code: "USERNAME_DUPLICATED", + code: "USERNAME_IS_ALREADY_TAKEN", + }); + }); + + test("should fail when email already exists", async () => { + const payload = { + username: "first_user", + password: "password123", + email: "duplicated@example.com", + }; + + // Create the first user + await api.v2.user.post(payload); + + // Attempt to register with the same username + const { error, status } = await api.v2.user.post({ + ...payload, + username: "another_user", + }); + + // Should return 409 Conflict + expect(status).toBe(409); + expect(error?.value).toMatchObject({ + code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", }); }); @@ -67,10 +103,11 @@ describe("Registration E2E Tests - POST /v2/user", () => { email: "invalid-email-format", // Invalid email }); - // Should be caught by Zod validation in SignupRequestSchema + // Malformed token should be seen as unauthorized expect(status).toBe(422); - // @ts-expect-error tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) - expect(error?.value?.code).toBe("VALIDATION_ERROR"); + expect(error?.value).toMatchObject({ + code: "VALIDATION_ERROR", + }); }); }); @@ -88,6 +125,8 @@ describe("Profile E2E Tests - GET /v2/me", () => { const signup = await api.v2.user.post({ username: "profile_user", password: "password123", + email: "first@example.com", + displayName: "John Doe", }); const token = signup.data?.data.token; @@ -103,6 +142,8 @@ describe("Profile E2E Tests - GET /v2/me", () => { expect(status).toBe(200); expect(data).toMatchObject({ username: "profile_user", + email: "first@example.com", + displayName: "John Doe", }); expect(data?.id).toBeString(); }); @@ -116,8 +157,9 @@ describe("Profile E2E Tests - GET /v2/me", () => { // Malformed token should be seen as unauthorized expect(status).toBe(401); - // @ts-expect-error – tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) - expect(error?.value.code).toBe("UNAUTHORIZED"); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); }); test("should return 401 for non-existent session", async () => { @@ -139,30 +181,4 @@ describe("Profile E2E Tests - GET /v2/me", () => { expect(status).toBe(401); }); - - test("should reject a validly-shaped token with the wrong secret", async () => { - const signup = await api.v2.user.post({ - username: "wrong_secret_user", - password: "password123", - }); - - const token = signup.data?.data.token; - expect(token).toBeDefined(); - if (!token) { - throw new Error("Expected signup to return a token"); - } - - const [sessionId] = token.split("."); - const wrongSecretToken = `${sessionId}.000000000000000000000000000000`; - - const { status, error } = await api.v2.me.get({ - headers: { - authorization: `Bearer ${wrongSecretToken}`, - }, - }); - - expect(status).toBe(401); - // @ts-expect-error – tracked in elysiajs/elysia#1248 (error type inference still broken for custom onError) - expect(error?.value.code).toBe("UNAUTHORIZED"); - }); }); diff --git a/apps/backend/tests/integration/repositories/SessionRepository.test.ts b/apps/backend/tests/integration/repositories/SessionRepository.test.ts deleted file mode 100644 index 810d7bb..0000000 --- a/apps/backend/tests/integration/repositories/SessionRepository.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, test, expect, beforeEach, afterAll } from "bun:test"; -import { createSessionRepository } from "@repositories/SessionRepository"; -import { createUserRepository } from "@repositories/UserRepository"; -import { prisma } from "@lib/prisma"; - -/** - * SessionRepository Integration Test - * Validates session lifecycle, SHA-256 hashing, and relational integrity. - */ -describe("SessionRepository Integration Test", () => { - const sessionRepo = createSessionRepository(prisma); - const userRepo = createUserRepository(prisma); - - // Clean up database to ensure isolation - beforeEach(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - }); - - afterAll(async () => { - await prisma.$disconnect(); - }); - - test("should create a session with a hashed secret and return the plain secret", async () => { - // 1. Setup: A session requires a valid User ID due to FK constraints - const user = await userRepo.create({ - id: "SESSU01", - username: "session_user", - password: "password123", - }); - - const sessionData = { - userId: user.id, - ipAddress: "192.168.1.1", - userAgent: "Bun-Runtime", - }; - - // 2. Execution - const session = await sessionRepo.create(sessionData); - - // 3. Assertions - expect(session.id).toBeDefined(); - expect(session.userId).toBe(user.id); - - // Ensure the 'secret' is returned plain (for token generation) - // but the 'secretHash' is what actually lives in the DB - expect(session.secret).toBeDefined(); - expect(session.secret).toHaveLength(30); // 15 bytes hex = 30 chars - expect(session.secretHash).not.toBe(session.secret); - - // Verify DB persistence - const dbSession = await prisma.session.findUnique({ - where: { id: session.id }, - }); - expect(dbSession).not.toBeNull(); - expect(dbSession?.secretHash).toBe(session.secretHash); - }); - - test("should find a session by ID and matching secret hash", async () => { - const user = await userRepo.create({ - id: "AUTHU01", - username: "auth_user", - password: "pwd", - }); - const session = await sessionRepo.create({ userId: user.id }); - - // Attempt to find using the correct hash generated during creation - const found = await sessionRepo.findByIdAndSecretHash(session.id, session.secretHash); - - expect(found).not.toBeNull(); - expect(found?.id).toBe(session.id); - }); - - test("should return null if the secret hash does not match", async () => { - const user = await userRepo.create({ - id: "SECU01A", - username: "secure_user", - password: "pwd", - }); - const session = await sessionRepo.create({ userId: user.id }); - - // Attempt to find using an incorrect hash - const found = await sessionRepo.findByIdAndSecretHash(session.id, "wrong_hash"); - - expect(found).toBeNull(); - }); - - test("should maintain referential integrity (Foreign Key)", async () => { - // Attempting to create a session for a non-existent user should fail at DB level - expect(sessionRepo.create({ userId: "NON_EXISTENT_USER" })).rejects.toThrow(); - }); -}); diff --git a/apps/backend/tests/integration/repositories/UserRepository.test.ts b/apps/backend/tests/integration/repositories/UserRepository.test.ts deleted file mode 100644 index cdefbc9..0000000 --- a/apps/backend/tests/integration/repositories/UserRepository.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, test, expect, beforeEach, afterAll } from "bun:test"; -import { createUserRepository } from "@repositories/UserRepository"; -import { prisma } from "@lib/prisma"; -import { Prisma } from "@project-cvsa/db"; - -/** - * UserRepository Integration Test - * Verifies database interaction for persistence-only repository behavior. - */ -describe("UserRepository Integration Test", () => { - const repo = createUserRepository(prisma); - - // Clean up the user table before each test to ensure a predictable state - beforeEach(async () => { - await prisma.user.deleteMany(); - }); - - // Close database connection after all tests are finished - afterAll(async () => { - await prisma.$disconnect(); - }); - - test("should persist a new user to the database", async () => { - const userData = { - id: "INTEG01", - username: "integration_tester", - password: "hashed_password", - displayName: "Integration Bot", - email: "bot@example.com", - }; - - const user = await repo.create(userData); - - // Verify returned object properties - expect(user.id).toBe(userData.id); - expect(user.username).toBe(userData.username); - expect(user.password).toBe(userData.password); - - // Verify database record actually exists - const dbUser = await prisma.user.findUnique({ where: { id: user.id } }); - expect(dbUser).not.toBeNull(); - expect(dbUser?.username).toBe(userData.username); - expect(dbUser?.password).toBe(userData.password); - }); - - test("should throw Prisma P2002 when username is duplicated", async () => { - const userData = { - username: "duplicate_user", - password: "hashed_password", - }; - - // Create the first user - await repo.create({ ...userData, id: "DUPUSR1" }); - - // Repository now only persists data and leaves business error mapping to service - try { - await repo.create({ ...userData, id: "DUPUSR2" }); - expect.unreachable(); - } catch (e) { - expect(e).toBeInstanceOf(Prisma.PrismaClientKnownRequestError); - const err = e as Prisma.PrismaClientKnownRequestError; - expect(err.code).toBe("P2002"); - } - }); - - test("should find a user by email correctly", async () => { - const email = "search@example.com"; - await repo.create({ - id: "SEARCH1", - username: "searcher", - password: "hashed_password", - email, - }); - - const foundUser = await repo.findByEmail(email); - expect(foundUser).not.toBeNull(); - expect(foundUser?.email).toBe(email); - }); - - test("should find a user by username correctly", async () => { - const username = "searcher"; - await repo.create({ - id: "SEARCH2", - username, - password: "hashed_password", - email: "search@example.com", - }); - - const foundUser = await repo.findByUsername(username); - expect(foundUser).not.toBeNull(); - expect(foundUser?.username).toBe(username); - }); - - test("should return null when searching for non-existent user", async () => { - const user = await repo.findById("NON_EXISTENT_ID"); - expect(user).toBeNull(); - }); -}); diff --git a/apps/backend/tests/unit/lib/conflictError.test.ts b/apps/backend/tests/unit/lib/conflictError.test.ts deleted file mode 100644 index 51caa0d..0000000 --- a/apps/backend/tests/unit/lib/conflictError.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, test, expect } from "bun:test"; -import { ConflictError } from "@lib/error/conflict"; - -describe("ConflictError", () => { - test("Has correct default value", () => { - const err = new ConflictError("username"); - - expect(err).toBeInstanceOf(Error); - expect(err.code).toBe("APP_CONFLICT_ERROR"); - expect(err.statusCode).toBe(409); - expect(err.meta).toEqual({ field: "username" }); - expect(err.message).toBe("Record already exists"); - }); - - test("Can override code and message", () => { - const err = new ConflictError("email", "EMAIL_DUPLICATED", "Email registered", 409); - - expect(err.code).toBe("EMAIL_DUPLICATED"); - expect(err.message).toBe("Email registered"); - }); -}); diff --git a/apps/backend/tests/unit/services/AuthService.test.ts b/apps/backend/tests/unit/services/AuthService.test.ts deleted file mode 100644 index 13b0777..0000000 --- a/apps/backend/tests/unit/services/AuthService.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, test, expect, mock, beforeEach } from "bun:test"; -import { AuthService } from "@services/AuthService"; -import type { - IUserRepository, - ISessionRepository, - SessionWithSecret, -} from "@repositories/interfaces"; -import type { User } from "@project-cvsa/db"; -import { Prisma } from "@project-cvsa/db"; - -function hashSecret(secret: string): string { - const hasher = new Bun.CryptoHasher("sha256"); - hasher.update(secret); - return hasher.digest("hex"); -} - -/** - * AuthService Unit Test - * * This suite uses strict typing to ensure that our mocks satisfy - * the IUserRepository and ISessionRepository interfaces without - * resorting to 'any' type casting. - */ -describe("AuthService Unit Test (Strict Types)", () => { - let mockUserRepo: IUserRepository; - let mockSessionRepo: ISessionRepository; - let authService: AuthService; - - // Standardized mock data matching Prisma generated types - const mockUser: User = { - id: "BX23Q95", - username: "testuser", - password: "hashed_password", - displayName: "Test User", - email: "test@example.com", - roleId: null, - reputation: 0, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - }; - - const mockSession: SessionWithSecret = { - id: "0123456789abcdef0123456789abcd", - userId: "BX23Q95", - secretHash: hashSecret("abcdef0123456789abcdef01234567"), - secret: "abcdef0123456789abcdef01234567", - userAgent: "Mozilla/5.0", - ipAddress: "127.0.0.1", - createdAt: new Date(), - lastVerifiedAt: new Date(), - }; - - beforeEach(() => { - // We define the mocks explicitly to satisfy the interface requirements - mockUserRepo = { - create: mock(async () => mockUser), - findById: mock(async (id: string) => (id === mockUser.id ? mockUser : null)), - findByUsername: mock(async () => null), - findByEmail: mock(async () => null), - }; - - mockSessionRepo = { - create: mock(async () => mockSession), - findById: mock(async (id: string) => (id === mockSession.id ? mockSession : null)), - findByIdAndSecretHash: mock(async (id: string, secretHash: string) => - id === mockSession.id && secretHash === mockSession.secretHash ? mockSession : null - ), - }; - - // Inject the strictly typed mocks into the service - authService = new AuthService(mockUserRepo, mockSessionRepo); - }); - - describe("register()", () => { - test("should register user and return token without type assertions", async () => { - const signupData = { - username: "testuser", - password: "securePassword123", - displayName: "Test User", - }; - - const result = await authService.register(signupData, "127.0.0.1", "Bun-Test"); - - // Verify the logic flow matches our architectural requirements - expect(mockUserRepo.create).toHaveBeenCalled(); - expect(mockSessionRepo.create).toHaveBeenCalled(); - expect(result.token).toBe(`${mockSession.id}.${mockSession.secret}`); - expect(result.user.id).toBe(mockUser.id); - }); - - test("should execute password verification padding before surfacing duplicate username errors", async () => { - const duplicateError = new Prisma.PrismaClientKnownRequestError( - "Unique constraint failed", - { - code: "P2002", - clientVersion: "test", - meta: { - driverAdapterError: { - cause: { - constraint: { - fields: ["username"], - }, - }, - }, - }, - } - ); - - const verifySpy = mock(async () => true); - - mockUserRepo.create = mock(async () => { - throw duplicateError; - }); - authService = new AuthService(mockUserRepo, mockSessionRepo, undefined, { - passwordVerify: verifySpy, - }); - - expect( - authService.register({ - username: "testuser", - password: "securePassword123", - }) - ).rejects.toMatchObject({ - code: "USERNAME_DUPLICATED", - }); - - expect(verifySpy).toHaveBeenCalledTimes(1); - }); - }); - - describe("verifyToken()", () => { - test("should return user for valid token format", async () => { - const validToken = `${mockSession.id}.${mockSession.secret}`; - - const result = await authService.verifyToken(validToken); - - // Ensure the service correctly hashes the secret and queries repositories - expect(result).not.toBeNull(); - expect(result?.id).toBe(mockUser.id); - expect(mockSessionRepo.findById).toHaveBeenCalledWith(mockSession.id); - expect(mockSessionRepo.findByIdAndSecretHash).not.toHaveBeenCalled(); - }); - - test("should return null for malformed tokens after performing padded lookup", async () => { - const result = await authService.verifyToken("invalid-format"); - expect(result).toBeNull(); - expect(mockSessionRepo.findById).toHaveBeenCalledTimes(1); - }); - - test("should return null if repository finds no session", async () => { - mockSessionRepo.findById = mock(async () => null); - authService = new AuthService(mockUserRepo, mockSessionRepo); - - const result = await authService.verifyToken(`${mockSession.id}.${mockSession.secret}`); - expect(result).toBeNull(); - }); - - test("should return null when secret does not match an existing session", async () => { - const result = await authService.verifyToken( - `${mockSession.id}.000000000000000000000000000000` - ); - expect(result).toBeNull(); - expect(mockUserRepo.findById).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index d1d8541..f2c10c3 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,15 +1,22 @@ { - "extends": "@project-cvsa/configs/tsconfig/server.json", "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext"], + "module": "preserve", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "incremental": true, + "resolveJsonModule": true, "paths": { - "@routes/*": ["./src/routes/*"], + "@common/*": ["./src/common/*"], + "@modules/*": ["./src/modules/*"], "@lib/*": ["./src/lib/*"], - "@repositories/*": ["./src/repositories/*"], - "@services/*": ["./src/services/*"], - "@controllers/*": ["./src/controllers/*"], - "@schemas/*": ["./src/schemas/*"], - "@plugins/*": ["./src/plugins/*"], "@/*": ["./src/*"] - }, + } } } diff --git a/bun.lock b/bun.lock index a93320c..0f875fa 100644 --- a/bun.lock +++ b/bun.lock @@ -12,12 +12,14 @@ "name": "@project-cvsa/backend", "version": "0.0.1", "dependencies": { + "@better-auth/prisma-adapter": "^1.5.4", "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", "@project-cvsa/configs": "workspace:*", "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", + "better-auth": "^1.5.4", "chalk": "^5.6.2", "elysia": "^1.4.26", "elysia-ip": "^1.0.10", @@ -55,6 +57,24 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-4M4nMAWrDd3TmpV6dONkJjybBVKRZghe5Oj0NNyDEoXubxastQdO7Sb5B54I1rTx5yoMgsqaB+kbJnu/9UgjQg=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-DPww7rIfz6Ed7dZlJSW9xMQ42VKaJLB5Cs+pPqd+UHKRyighKjf3VgvMIcAdFPc4olQ0qRHo3+ZJhFlBCxRhxA=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0" } }, "sha512-iiWYut9rbQqiAsgRBtj6+nxanwjapxRgpIJbiS2o81h7b9iclE0AiDA0Foes590gdFQvskNauZcCpuF8ytxthg=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-ArzJN5Obk6i6+vLK1HpPzLIcsjxZYXPPUvxVU8eyU5HyoUT2MlswWfPQ8UJAKPn0iq/T4PVp/wZcQMhWk1tuNA=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ZQTbcBopw/ezjjbNFsfR3CRp0QciC4tJCarAnB5G9fZtUYbDjfY0vZOxIRmU4kI3x755CXQpGqTrkwmXaMRa3w=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.4", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.4" } }, "sha512-mGXTY7Ecxo7uvlMr6TFCBUvlH0NUMOeE9LKgPhG4HyhBN6VfCEg/DD9PG0Z2IatmMWQbckkt7ox5A0eBpG9m5w=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], @@ -95,8 +115,14 @@ "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], @@ -155,10 +181,20 @@ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], + "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "better-auth": ["better-auth@1.5.4", "", { "dependencies": { "@better-auth/core": "1.5.4", "@better-auth/drizzle-adapter": "1.5.4", "@better-auth/kysely-adapter": "1.5.4", "@better-auth/memory-adapter": "1.5.4", "@better-auth/mongo-adapter": "1.5.4", "@better-auth/prisma-adapter": "1.5.4", "@better-auth/telemetry": "1.5.4", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-ReykcEKx6Kp9560jG1wtlDBnftA7L7xb3ZZdDWm5yGXKKe2pUf+oBjH0fqekrkRII0m4XBVQbQ0mOrFv+3FdYg=="], + + "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + + "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -195,6 +231,8 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], @@ -243,6 +281,10 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], + + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -253,6 +295,12 @@ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + + "mongodb": ["mongodb@7.1.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg=="], + + "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], @@ -261,6 +309,8 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanostores": ["nanostores@1.1.1", "", {}, "sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg=="], + "node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -313,6 +363,8 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -329,6 +381,8 @@ "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -337,12 +391,16 @@ "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], @@ -355,6 +413,8 @@ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "try": ["try@1.0.3", "", {}, "sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -381,6 +441,10 @@ "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], diff --git a/packages/config/package.json b/packages/config/package.json deleted file mode 100644 index 69ae86c..0000000 --- a/packages/config/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@project-cvsa/configs", - "exports": { - "./tsconfig/*": "./tsconfig/*.json" - } -} diff --git a/packages/config/tsconfig/base.json b/packages/config/tsconfig/base.json deleted file mode 100644 index 00d78f8..0000000 --- a/packages/config/tsconfig/base.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "bundler", - "isolatedModules": true, - "incremental": true, - "resolveJsonModule": true - } -} diff --git a/packages/config/tsconfig/dom.json b/packages/config/tsconfig/dom.json deleted file mode 100644 index 4fa7758..0000000 --- a/packages/config/tsconfig/dom.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "target": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "module": "ESNext", - "jsx": "preserve", - "moduleResolution": "bundler" - } -} diff --git a/packages/config/tsconfig/server.json b/packages/config/tsconfig/server.json deleted file mode 100644 index aaa48a4..0000000 --- a/packages/config/tsconfig/server.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "target": "ESNext", - "lib": ["ESNext"], - "module": "preserve", - "allowImportingTsExtensions": true, - "noEmit": true - } -} diff --git a/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql b/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql new file mode 100644 index 0000000..4e1ea4e --- /dev/null +++ b/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql @@ -0,0 +1,80 @@ +/* + Warnings: + + - You are about to drop the column `last_verified_at` on the `session` table. All the data in the column will be lost. + - You are about to drop the column `secret_hash` on the `session` table. All the data in the column will be lost. + - You are about to drop the column `deleted_at` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `display_name` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `password` on the `user` table. All the data in the column will be lost. + - A unique constraint covering the columns `[token]` on the table `session` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[display_username]` on the table `user` will be added. If there are existing duplicate values, this will fail. + - Added the required column `expiresAt` to the `session` table without a default value. This is not possible if the table is not empty. + - Added the required column `token` to the `session` table without a default value. This is not possible if the table is not empty. + - Added the required column `updated_at` to the `session` table without a default value. This is not possible if the table is not empty. + - Added the required column `display_username` to the `user` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "auth"."session" DROP COLUMN "last_verified_at", +DROP COLUMN "secret_hash", +ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL, +ADD COLUMN "token" TEXT NOT NULL, +ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "auth"."user" DROP COLUMN "deleted_at", +DROP COLUMN "display_name", +DROP COLUMN "password", +ADD COLUMN "display_username" TEXT NOT NULL, +ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "image" TEXT, +ADD COLUMN "name" TEXT; + +-- CreateTable +CREATE TABLE "auth"."account" ( + "id" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "accessToken" TEXT, + "refreshToken" TEXT, + "idToken" TEXT, + "accessTokenExpiresAt" TIMESTAMP(3), + "refreshTokenExpiresAt" TIMESTAMP(3), + "scope" TEXT, + "password" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth"."verification" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "value" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "verification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "account_userId_idx" ON "auth"."account"("userId"); + +-- CreateIndex +CREATE INDEX "verification_identifier_idx" ON "auth"."verification"("identifier"); + +-- CreateIndex +CREATE INDEX "session_user_id_idx" ON "auth"."session"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "session_token_key" ON "auth"."session"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_display_username_key" ON "auth"."user"("display_username"); + +-- AddForeignKey +ALTER TABLE "auth"."account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "auth"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/models/auth/account.prisma b/packages/db/prisma/models/auth/account.prisma new file mode 100644 index 0000000..4aea913 --- /dev/null +++ b/packages/db/prisma/models/auth/account.prisma @@ -0,0 +1,20 @@ +model Account { + id String @id + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("account") + @@schema("auth") +} diff --git a/packages/db/prisma/models/auth/session.prisma b/packages/db/prisma/models/auth/session.prisma index bca0256..4fe562f 100644 --- a/packages/db/prisma/models/auth/session.prisma +++ b/packages/db/prisma/models/auth/session.prisma @@ -1,13 +1,16 @@ model Session { - id String @id - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - ipAddress String? @map("ip_address") - userAgent String? @map("user_agent") - secretHash String @map("secret_hash") - createdAt DateTime @default(now()) @map("created_at") - lastVerifiedAt DateTime @default(now()) @map("last_verified_at") + id String @id + token String + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + expiresAt DateTime + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + @@unique([token]) + @@index([userId]) @@map("session") @@schema("auth") } diff --git a/packages/db/prisma/models/auth/user.prisma b/packages/db/prisma/models/auth/user.prisma index f3260cc..c3bb10f 100644 --- a/packages/db/prisma/models/auth/user.prisma +++ b/packages/db/prisma/models/auth/user.prisma @@ -1,13 +1,16 @@ model User { id String @id + name String? username String @unique - displayName String? @map("display_name") + displayUsername String @unique @map("display_username") email String? @unique - password String + emailVerified Boolean @default(false) @map("email_verified") + image String? changesMade History[] role Role? @relation(fields: [roleId], references: [id]) roleId Int? @map("role_id") sessions Session[] + accounts Account[] artist Artist? reputation Int @default(0) reputationHistories ReputationHistory[] @@ -15,7 +18,6 @@ model User { files File[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") @@map("user") @@schema("auth") diff --git a/packages/db/prisma/models/auth/verification.prisma b/packages/db/prisma/models/auth/verification.prisma new file mode 100644 index 0000000..a465808 --- /dev/null +++ b/packages/db/prisma/models/auth/verification.prisma @@ -0,0 +1,12 @@ +model Verification { + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([identifier]) + @@map("verification") + @@schema("auth") +} From 671a23698f00fe831952f6816a184d9bc693d02a Mon Sep 17 00:00:00 2001 From: alikia2x Date: Thu, 12 Mar 2026 03:27:50 +0800 Subject: [PATCH 04/37] feat(auth): set cookie for token on user creation (#12) The POST /user endpoint now returns a Set-Cookie header upon successful registration. This allows for immediate session persistence without requiring a separate login step. --- apps/backend/src/modules/auth/routes/signup.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/modules/auth/routes/signup.ts b/apps/backend/src/modules/auth/routes/signup.ts index 233feda..fa983d5 100644 --- a/apps/backend/src/modules/auth/routes/signup.ts +++ b/apps/backend/src/modules/auth/routes/signup.ts @@ -7,6 +7,8 @@ import { RateLimitError, AppError } from "@common/error"; import { auth } from "@modules/auth/lib"; import { getRandomId } from "@common/utils"; +const DAY = 86400; + export const signupHandler = new Elysia() .use(ip()) .use( @@ -20,7 +22,7 @@ export const signupHandler = new Elysia() ) .post( "/user", - async ({ body, status, headers }) => { + async ({ body, status, headers, cookie: { token: tokenCookie } }) => { const { user, token } = await auth.api.signUpEmail({ body: { name: body.displayName || body.username, @@ -37,6 +39,12 @@ export const signupHandler = new Elysia() }); } + tokenCookie.value = token; + tokenCookie.httpOnly = true; + tokenCookie.maxAge = 90 * DAY; + tokenCookie.secure = true; + tokenCookie.sameSite = "lax"; + return status(200, { message: "Successfully registered", data: { From b8d7f0524912976e9d1cc60ea9d98b604233e540 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Tue, 17 Mar 2026 22:07:32 +0800 Subject: [PATCH 05/37] feat(song): implement song CRUD endpoints (#17) * refactor: move common logic and code into the `core` package - Created `packages/core` with configurations for TypeScript and dependencies. - Moved environment variables, Prisma client initialization, and utility functions to the core package. - Relocated error response schemas and authentication modules. - Introduced DTOs to better exchage objects and entities across different modules. * feat: add Song module with DTO, Repository, Service and REST API Updated Prisma schema - add performance model, rename singerOfSong to performance * chore: update tsconfig files, add AGENTS.md and move test files * feat(song): implement song CRUD endpoints - Add POST /song/:id/details endpoint to get song details - Add POST /song endpoint to create new song - Add PATCH /song/:id endpoint to update song - Add DELETE /song/:id endpoint to delete song - Add GET /song endpoint to list songs - Add ErrorResponseDto for consistent error handling - Update AGENTS.md with module-specific backend conventions --- .agents/skills/jujutsu-vcs/SKILL.md | 190 +++++++++++++ .vscode/settings.json | 32 +-- .zed/settings.json | 72 +++++ AGENTS.md | 116 ++++++++ README.md | 4 +- apps/backend/package.json | 71 +++-- apps/backend/src/common/error/AppError.ts | 12 - .../backend/src/common/error/ConflictError.ts | 2 +- .../src/common/error/RateLimitError.ts | 2 +- .../src/common/error/getErrorResponse.ts | 12 + apps/backend/src/common/error/index.ts | 2 +- apps/backend/src/common/middlewares/auth.ts | 9 +- apps/backend/src/common/schemas/index.ts | 1 - apps/backend/src/errorHandler.ts | 37 +-- apps/backend/src/index.ts | 16 +- .../modules/auth/{routes => }/currentUser.ts | 21 +- apps/backend/src/modules/auth/index.ts | 6 +- apps/backend/src/modules/auth/routes/index.ts | 5 - .../src/modules/auth/schema/currentUser.ts | 10 - apps/backend/src/modules/auth/schema/index.ts | 2 - .../backend/src/modules/auth/schema/signup.ts | 19 -- .../src/modules/auth/{routes => }/signup.ts | 43 ++- .../src/modules/catalog/song/create.ts | 25 ++ .../src/modules/catalog/song/delete.ts | 31 ++ .../src/modules/catalog/song/details.ts | 35 +++ .../backend/src/modules/catalog/song/index.ts | 5 + apps/backend/src/modules/catalog/song/list.ts | 20 ++ .../src/modules/catalog/song/update.ts | 32 +++ apps/backend/src/modules/index.ts | 1 + apps/backend/tests/{e2e => }/auth.test.ts | 2 +- apps/backend/tests/song.test.ts | 265 ++++++++++++++++++ apps/backend/tsconfig.json | 31 +- biome.json | 86 +++--- bun.lock | 100 +++++-- opencode.json | 14 + package.json | 39 +-- packages/core/.env.example | 1 + packages/core/common/env.ts | 13 + packages/core/common/error/AppError.ts | 28 ++ packages/core/common/error/index.ts | 1 + packages/core/common/index.ts | 2 + packages/core/common/internal.ts | 9 + packages/core/common/prisma.ts | 9 + .../core/common/schema/error.ts | 4 +- packages/core/common/schema/index.ts | 1 + .../core}/common/utils/index.ts | 0 .../core}/common/utils/randomId.ts | 0 packages/core/index.ts | 2 + .../core/modules/auth/betterAuth.ts | 8 +- packages/core/modules/auth/index.ts | 2 + packages/core/modules/auth/user/dto.ts | 86 ++++++ packages/core/modules/auth/user/index.ts | 1 + packages/core/modules/catalog/index.ts | 1 + packages/core/modules/catalog/song/dto.ts | 97 +++++++ packages/core/modules/catalog/song/index.ts | 3 + .../catalog/song/repository.interface.ts | 14 + .../core/modules/catalog/song/repository.ts | 179 ++++++++++++ packages/core/modules/catalog/song/service.ts | 49 ++++ packages/core/modules/index.ts | 2 + packages/core/package.json | 32 +++ .../tests/integration/SongRepository.test.ts | 190 +++++++++++++ packages/core/tests/unit/SongService.test.ts | 56 ++++ packages/core/tsconfig.json | 4 + packages/db/package.json | 50 ++-- packages/db/prisma/models/core/artist.prisma | 18 +- .../db/prisma/models/core/artistRole.prisma | 15 +- .../core/performance.prisma} | 6 +- packages/db/prisma/models/core/singer.prisma | 24 +- packages/db/prisma/models/core/song.prisma | 6 +- .../db/prisma/models/core/svsEngine.prisma | 3 +- .../models/core/svsEngineVersion.prisma | 19 +- .../db/prisma/models/core/voicebank.prisma | 2 +- .../db/prisma/relations/core/creation.prisma | 16 ++ .../relations/core/singerSvsEngine.prisma | 13 - .../core/singerSvsEngineVersion.prisma | 13 - packages/db/tsconfig.json | 4 + packages/typescript-config/base.json | 12 + packages/typescript-config/package.json | 4 + turbo.json | 32 ++- 79 files changed, 2011 insertions(+), 390 deletions(-) create mode 100644 .agents/skills/jujutsu-vcs/SKILL.md create mode 100644 .zed/settings.json create mode 100644 AGENTS.md delete mode 100644 apps/backend/src/common/error/AppError.ts create mode 100644 apps/backend/src/common/error/getErrorResponse.ts delete mode 100644 apps/backend/src/common/schemas/index.ts rename apps/backend/src/modules/auth/{routes => }/currentUser.ts (50%) delete mode 100644 apps/backend/src/modules/auth/routes/index.ts delete mode 100644 apps/backend/src/modules/auth/schema/currentUser.ts delete mode 100644 apps/backend/src/modules/auth/schema/index.ts delete mode 100644 apps/backend/src/modules/auth/schema/signup.ts rename apps/backend/src/modules/auth/{routes => }/signup.ts (59%) create mode 100644 apps/backend/src/modules/catalog/song/create.ts create mode 100644 apps/backend/src/modules/catalog/song/delete.ts create mode 100644 apps/backend/src/modules/catalog/song/details.ts create mode 100644 apps/backend/src/modules/catalog/song/index.ts create mode 100644 apps/backend/src/modules/catalog/song/list.ts create mode 100644 apps/backend/src/modules/catalog/song/update.ts rename apps/backend/tests/{e2e => }/auth.test.ts (100%) create mode 100644 apps/backend/tests/song.test.ts create mode 100644 opencode.json create mode 100644 packages/core/.env.example create mode 100644 packages/core/common/env.ts create mode 100644 packages/core/common/error/AppError.ts create mode 100644 packages/core/common/error/index.ts create mode 100644 packages/core/common/index.ts create mode 100644 packages/core/common/internal.ts create mode 100644 packages/core/common/prisma.ts rename apps/backend/src/common/schemas/ErrorResponseSchema.ts => packages/core/common/schema/error.ts (55%) create mode 100644 packages/core/common/schema/index.ts rename {apps/backend/src => packages/core}/common/utils/index.ts (100%) rename {apps/backend/src => packages/core}/common/utils/randomId.ts (100%) create mode 100644 packages/core/index.ts rename apps/backend/src/modules/auth/lib/index.ts => packages/core/modules/auth/betterAuth.ts (83%) create mode 100644 packages/core/modules/auth/index.ts create mode 100644 packages/core/modules/auth/user/dto.ts create mode 100644 packages/core/modules/auth/user/index.ts create mode 100644 packages/core/modules/catalog/index.ts create mode 100644 packages/core/modules/catalog/song/dto.ts create mode 100644 packages/core/modules/catalog/song/index.ts create mode 100644 packages/core/modules/catalog/song/repository.interface.ts create mode 100644 packages/core/modules/catalog/song/repository.ts create mode 100644 packages/core/modules/catalog/song/service.ts create mode 100644 packages/core/modules/index.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/tests/integration/SongRepository.test.ts create mode 100644 packages/core/tests/unit/SongService.test.ts create mode 100644 packages/core/tsconfig.json rename packages/db/prisma/{relations/core/singerOfSong.prisma => models/core/performance.prisma} (92%) create mode 100644 packages/db/prisma/relations/core/creation.prisma delete mode 100644 packages/db/prisma/relations/core/singerSvsEngine.prisma delete mode 100644 packages/db/prisma/relations/core/singerSvsEngineVersion.prisma create mode 100644 packages/db/tsconfig.json create mode 100644 packages/typescript-config/base.json create mode 100644 packages/typescript-config/package.json diff --git a/.agents/skills/jujutsu-vcs/SKILL.md b/.agents/skills/jujutsu-vcs/SKILL.md new file mode 100644 index 0000000..883f821 --- /dev/null +++ b/.agents/skills/jujutsu-vcs/SKILL.md @@ -0,0 +1,190 @@ +--- +name: jujutsu-vcs +description: The guide of using Jujutsu instead of Git for version control. Always activate FIRST on any git/VCS operations (commit, status, branch, push, etc.). DO NOT IGNORE. +allowed-tools: Bash(jj *) +--- + +This skill helps you work with Jujutsu, a Git-compatible VCS featuring a simpler workflow, a more user-friendly CLI, and a lower risk of data loss. +This skill does not include instructions on workflow; it only introduces jj and teaches you how to use it. You should follow the instructions provided by the user, or the workflow preferences indicated elsewhere in the context. +By default, jj use Git as its storage backend. This means commits, trees, blobs, and file contents are stored in a regular Git repository for full compatibility with Git remotes, hosting platforms, and existing Git tools, while jj-specific metadata—like bookmarks, the operation log, and change IDs—is kept separately in the .jj/ directory. + +## Identifing JJ Availability + +The `.jj` dir excludes itself by `.gitignore` and does not share across the team. +This means you need to separately check whether JJ is available on the user’s computer. +Sometimes, the agent tool may not list `.jj` when you try to list files or dirs in the project because it’s in `.gitignore`. +Hence, running `jj` command is always the most reliable way to determine whether this is a jj repository. +If you got output like `Error: There is no jj repo in "."` or "command not found", this is not a jj repo. + +## Key concepts + +- **Change**: A logical unit of work with a stable identifier. It represents ongoing development of a feature, fix, or piece of functionality +- **Change ID**: A unique identifier for a change. You can refer them in a short form of few letters in k-z range (e.g. `uvzonqtw`) +- **Bookmark**: A bookmark is a named pointer to a commit. They are similar to Git’s branches and even more similar to Mercurial’s bookmarks. Unlike in Git, there is no concept of a "current bookmark"; bookmarks do not move when you create a new commit +- **Commit**: A snapshot of the files in the repository at a given point in time (technically a tree object), together with some metadata. The word "revision" is used as a synonym for "commit" +- **Working Copy**: Contains the files you’re currently working on. It is automatically snapshot at the beginning of almost every jj command, thus creating a new working-copy commit if any changes had been made in the working copy +- **Revsets**: An expression for selecting a set of commits +- **Filesets**: An expression for selecting a set of file + +## Filesets and Revsets + +In cheatsheet below, `[paths]` follows filesets syntax, and symbols like `@`, `x` and `y` can be replaced with any string that follows revsets syntax. +Note that when using filesets and revsets in command, you may need to enclose them in quotation marks to prevent them being accidentally escaped. + +### Revsets Syntax + +> Hint: Run `jj help -k revsets` to learn more + +`@`: working copy, and `@-` for its parent +Tag name or bookmark name like `v3.1.4` or `feat/my-feature` +ID like `uvzonqtw` means a specific commit +`x-`: parents of x +`x+`: children of x +`x::y`: descendants of x that are also ancestors of y. Same as git’s `--ancestry-path x..y` + +### Filesets Syntax + +> Hint: Run `jj help -k filesets` to learn more + +By default, the expression `path` is parsed as a glob pattern. + +`file:path`: Matches cwd-relative exact file path. +`glob:pattern`: Matches file paths with cwd-relative Unix-style shell wildcard pattern. +`glob-i:pattern`: Case insensitive glob. +`root:path`, `root-glob:pattern`, `root-file:path`: Similar to above, but relative to the workspace (project root) + +Examples: +`glob:*.c`: all .c files in the current working directory non-recursively. +`glob:**/*.c`: similar to above one, but matches recursively. +`src ~ glob:**/*.rs`: + +### Shared Syntax + +These syntax are available both in revsets and filesets. + +- `~` means not, so `~x` matches everything but `x` and `x ~ y` matches x but not y +- `&` means and, so `x & y` matches intersection of x and y +- `|` means or, so `x | y` matches union of x and y + +## Key differences from Git + +1. **Working Copy as a Commit**: No staging area. Files are automatically tracked and added to the current commit every time you run `jj` commands +2. **Effortless Jumping**: No stash or WIP commits needed. If you want to jump to another branch or commit, just `jj edit` it. There are no "uncommitted changes" blocking you +3. **Anonymous Branches**: Creating a named branch does not necessarily have to be your first step. You can naturally let the commit tree branch without having to name the branches +4. **Operation Log**: Every single action—every rebase, every description change, every file edit—is recorded in the operation log. You can run `jj undo` to instantly revert the last command, no matter how complex it was +5. **First-class Conflicts**: Conflicts are stored in the history. Conflicted states can be further rebased, merged, or backed out. You don’t have to resolve them before you work + +## Cheatsheet + +This cheatsheet is enough for common operations, organized by function. +JJ's help doc is comprehensive and easy to understand. Whenever you stuck at somewhere, you can always run `jj help` or pass the `--help` flag to any command to get help on how to use jj. + +> **Note**: Always use `--git` or `-s` with diff/show commands to prevent GUI clients from hanging. + +```bash +# Basic Operations +jj status # Show status of working copy, including files changed +jj log --limit n # Show recent n commits +jj log --revisions @ --no-graph --template "description" # See description of working copy + +# Diff (always use --git or -s to prevent GUI hang) +jj diff --git # Show diff of working copy vs parent +jj diff --git -r x # Show diff between x and its parent +jj diff --git [paths] # Show diff for specific paths +jj diff --git --from x --to y # Show diff from x to y +jj diff -s # Show only whether modified, added, or deleted + +# Show commit info (use --summary to prevent GUI hang) +jj show --summary x # Show summary of commit x + +# Creating & Modifying Commits +jj new # Create new change on top of current one +jj new main # Create new change on top of main bookmark +jj edit x # Move working copy to x (edit commit x) +jj describe -m "msg" # Edit commit message of working copy +jj describe -m "msg" -r x # Edit commit message of revision x +jj commit # Finalize working copy as a new commit +jj squash # Fold current change into its parent +jj restore [paths] # Restore paths in working copy to parent commit +jj split [paths] -m "msg" # Split commit: paths stay in older, rest go to newer +jj abandon # Abandon working copy change +jj abandon -r x # Abandon specific revision + +# Rebasing +jj rebase --source @ --onto x # Rebase current commit and descendants onto x +jj rebase --revisions x --insert-after y # Put x right after y + +# Bookmarks (like Git branches) +jj bookmark create my-feature # Create new bookmark +jj bookmark delete my-feature # Delete bookmark +jj bookmark list # List all bookmarks +# Note: Bookmarks don't auto-move on new commits. Update before pushing: +jj bookmark move my-feature --to @ # Move bookmark to working copy + +# File Operations +jj file list -r x # Show all files contained in commit x +jj file list -r x [paths] # Show paths contained in commit x +jj file untrack [paths] # Like `git rm --cached` +jj file annotate # Like `git blame` + +# Git Interop +jj git init [--no-colocate] # Initialize jj repo (optionally non-colocated) +jj git clone # Clone repository +jj git fetch # Fetch from remote (NO direct equivalent to git pull) +jj git push # Push to remote (auto force-push) +jj git push --bookmark develop # Push specific bookmark only + +# Operation Log (powerful undo/redo) +jj undo # Undo last operation +jj redo # Redo most recently undone operation +jj op log --limit n # Show recent n operations +jj op restore id # Restore repo to state of operation id +jj op diff --from x --to y # See what changed from operation x to y +jj op diff --from x --patch --git # See git-style diff since operation x +``` + +### Reading logs + +You can use `jj log` to see the history tree. You’d better use `jj log --limit n` to prevent context overflow. +A typical log looks like this: +``` +@ mwqpzkuz user@example.com 2026-03-12 10:16:57 main e60cfa44 +│ fix: a bug +│ ○ nynptron user@example.com 2026-03-14 15:45:45 develop 36b98762 +├─╯ feat: a new feature under development +◆ rvmwuqwt user@example.com 2026-03-11 12:35:07 git_head() 425d78a6 +│ feat: an amazing feature +``` +In which: +- `mwqpzkuz` is the change ID (consistent), while `e60cfa44` is the commit ID (same as Git’s commit ID). +- ◆ means immutable commit (usually it’s already pushed to the remote) +- @ means the working copy +- ○ means regular commit, you can modify it if you want. +- Ignore `git_head()`, it’s deprecated. + +### Splitting Commit + +You can use `jj split [paths]` command to split one commit into two commits, each contains a portion of the changes. Changes in files specified via `[paths]` will kept in older commit, while the rest will be present in newer commit. +Let’s assume that you want to split the current commit (x) into two commits: x and y (x is the parent of y). +If current commit has two files changed: `foo` and `bar`, and if you want the x contains `foo` and y contains `bar`, you can run: `jj split "foo" -m "commit message for x"`. This results in: + +Before +``` +@ x user@example.com (Files changed: `foo`, `bar`) +| (no description set) +``` +After +``` +@ y user@example.com (Files changed: `bar`) +| (no description set) +○ x user@example.com (Files changed: `foo`) +| commit message for x +``` + +## Additional Notes + +**IMPORTANT**: Read this section to prevent users from been fired! + +Although you can use `jj edit` to modify any commit, jj will prevent you from editing an immutable commit. Commits that have already been pushed to remote are usually immutable. +`jj git push` will automatically use force push, so the only way to prevent remote history from being overwritten is to avoid modifying immutable commits. +You can pass `--ignore-immutable` when jj hints you `Error: Commit x is immutable` to force-modify a commit, but you should only do this when the user explicitly tells you that you may do so. DO NOT pass `--ignore-immutable` without user’s approval. It’s as dangerous as Git force push. diff --git a/.vscode/settings.json b/.vscode/settings.json index 81fc238..0583de5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,18 @@ { - "editor.defaultFormatter": "biomejs.biome", - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[prisma]": { - "editor.defaultFormatter": "Prisma.prisma" - } + "editor.defaultFormatter": "biomejs.biome", + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + } } diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..b9f8297 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,72 @@ +{ + "languages": { + "Astro": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "CSS": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "HTML": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "JSON": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "JSONC": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "JavaScript": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "TSX": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + "TypeScript": { + "formatter": { + "language_server": { + "name": "biome" + } + } + }, + }, + "language_servers": [ + "!eslint", + "!typescript-language-server", + "..." + ], + "lsp": { + "biome": { + "settings": { + "require_config_file": true + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3f0d364 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,116 @@ +# System Prompt & Guide for AI Agents + +## 1. System Context +**Project:** CVSA (Chinese Vocal Synthesizer Archive) +**GitHub Repository**: [project-cvsa/cvsa](https://github.com/project-cvsa/cvsa) +**Repository Type:** Monorepo (Turborepo) +**Tech Stack:** Bun, Elysia, Prisma, PostgreSQL, Zod, TypeScript, Biome + +### Workspace Structure +- `apps/backend`: REST API (Elysia) +- `apps/frontend`: Web interface (Not yet implemented) +- `packages/db`: Prisma schema and generated database client +- `packages/core`: Shared business logic, DTOs, and Zod schemas + +## 2. Core Mental Model & Architecture + +When working on the backend, you MUST adhere to the Layered Architecture: `Handler -> Service -> Repository`. + +### Responsibility Isolation +1. **Handler (`apps/backend`)**: Only handles HTTP requests/responses, validates via Zod schemas, and calls Services. Never write business logic here. +2. **Service (`packages/core`)**: Contains pure business logic. Calls Repositories. Throws `AppError` on failure. +3. **Repository (`packages/core`)**: Implements pure database operations using Prisma. +4. **DTOs (`packages/core`)**: All request/response validation schemas and corresponding types exist here. + +### File Organization Mapping +If building a module named `[module]`, enforce this strict tree: +```text +apps/backend/src/modules/[module]/ +├── index.ts # Exports handlers +└── [handler].ts # Elysia routes + +packages/core/modules/[module]/ +├── index.ts # Public API exports +├── dto.ts # Zod schemas (Validation & Types) +├── service.ts # Business logic +├── repository.ts # Prisma DB operations +└── repository.interface.ts # DB contract +``` + +## 3. Strict Constraints & Safety + +These rules are absolute. Violation will cause pipeline or runtime failures. + +### Type Safety [CRITICAL] +- **[PROHIBITED]** `any` type: Never use `any`. +- **[PROHIBITED]** Inline type assertions: Do not use `as SomeType`, use type annotations. +- **[PROHIBITED]** `// @ts-ignore` or `// @ts-expect-error` in first-party code. (Only allowed in third-party code if a GitHub issue URL appended). +- **[PROHIBITED]** Defining local interfaces scoped to a single file for API contracts. +- **[REQUIRED]** Import database types directly from `@project-cvsa/db` (e.g., `Song`, `Artist`). + +### Validation & Zod +- **[PROHIBITED]** `z.any()` in any schema. +- **[PROHIBITED]** `import { t } from "elysia"`. Always use Zod, never Elysia's TypeBox. +- **[PROHIBITED]** Defining Zod schemas inline inside handlers. +- **[REQUIRED]** Define all Zod schemas in `packages/core/modules//dto.ts` first. +- **[REQUIRED]** Use `z.infer` to derive TypeScript types from Zod schemas. + +### Database & Error Handling +- **[PROHIBITED]** Modifying Prisma schema unless explicitly instructed by the user. +- **[PROHIBITED]** Using `try-catch` blocks in Elysia Handlers. +- **[REQUIRED]** Throw `AppError` directly from Services. + +## 4. Coding Standards + +### Naming Conventions +- **PascalCase**: Classes (`UserService`), Interfaces (`SignupRequest`), Types, and Class file names (`AppError.ts`). +- **camelCase**: Functions (`getUserById`), Variables, and standard file names (`authHandler.ts`). +- **UPPER_SNAKE_CASE**: Constants (`MAX_RATE_LIMIT`). + +### Import Hierarchy +Order imports from farthest to closest. Prioritize path aliases +1. External: Built-in and external NPM packages. +2. Workspace: Internal packages (`@project-cvsa/core`, `@project-cvsa/db`). +3. Aliases: `@/*`, `@modules/*`, `@common/*` pointing to `./src/`. +4. **[PROHIBITED]**: relative paths (`../`, `./`, `../../../`, etc.) + +### Module-Specific Information +- **Backend**: + 1. Our endpoints starts with `/v2` and this prefix is already configured in root handler. In all of the rest handlers, always use the complete path **without** the version prefix (e.g. `/song/:id/details`) + 2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful style. + +## 5. Execution Commands & Testing + +ALWAYS use `bun` as the package manager, test/command runner. + +### Global Monorepo Commands +```bash +bun lint # Check code issues via Biome +bun lint:fix # Auto-fix code issues +bun format # Format code via Biome +bun typecheck # Run tsc across all packages +# IMPORTANT: ALWAYS use `bun run test` +# [PROHIBITED]: Directly run `bun test` without `run` +bun run test # Run all tests globally +bun run test:coverage # Run all tests with coverage +``` + +### Module-Specific Testing Strategy +When writing tests, apply the following methodologies: +- **Backend (`apps/backend`)**: Write End-to-End (E2E) tests using Elysia Eden Treaty. +- **Core Services (`packages/core`)**: Write Unit tests. Mock the repository layer. +- **Core Repositories (`packages/core`)**: Write Integration tests connecting to a test database. + +## 6. Version Control & Collaboration + +### VCS Tooling (jujutsu / git) +- Prefer `jj` (Jujutsu) over `git` if available. Use "jujutsu-vcs" skill before performing VCS operations. +- If `jj` is unavailable, fallback to `git` with extreme caution. + +### Workflow & Branching +1. Branch from `develop`. +2. Format: `/--` (e.g., `fix/jack-132-song-api`). +3. Commit Message Format (Conventional Commits): + `(optional scope): ` + Types allowed: `feat`, `fix`, `update`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `chore`, `revert`. +4. PR Requirements: The PR title should be a commit message format (e.g. `feat(auth): implement forgot password`) that describe all changes within it. The PR description must contain `## Changes` and `## Related` sections. Maintainers will squash & merge. diff --git a/README.md b/README.md index edf28a9..9f472f3 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,13 @@ cvsa/ 3. **Set up environment variables** - Create `.env` files in each service directory following their `.env.example` templates: + Create `.env` and `.env.test` files in each service directory following their `.env.example` templates: ```bash cp apps/frontend/.env.example apps/frontend/.env + cp apps/frontend/.env.example apps/frontend/.env.test cp packages/db/.env.example packages/db/.env + cp packages/core/.env.example packages/core/.env.test ``` 4. **Initialize the database** diff --git a/apps/backend/package.json b/apps/backend/package.json index d93dad2..c4a556b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,38 +1,37 @@ { - "name": "@project-cvsa/backend", - "type": "module", - "version": "0.0.1", - "scripts": { - "lint": "bunx --bun biome lint", - "dev": "bun --hot src/index.ts", - "start": "bun src/index.ts", - "test": "bun test", - "test:coverage": "bun test --coverage", - "test:unit": "bun test tests/unit", - "test:e2e": "bun test tests/e2e", - "test:integration": "bun test tests/integration" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.4", - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@better-auth/prisma-adapter": "^1.5.4", - "@elysiajs/eden": "^1.0.5", - "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/configs": "workspace:*", - "@project-cvsa/db": "workspace:*", - "@t3-oss/env-core": "^0.13.10", - "argon2": "^0.44.0", - "better-auth": "^1.5.4", - "chalk": "^5.6.2", - "elysia": "^1.4.26", - "elysia-ip": "^1.0.10", - "elysia-rate-limit": "^4.5.0", - "nanoid": "^5.1.6", - "zod": "^4.3.6" - } + "name": "@project-cvsa/backend", + "type": "module", + "version": "0.0.1", + "scripts": { + "lint": "bunx --bun biome lint", + "dev": "bun --hot src/index.ts", + "start": "bun src/index.ts", + "test": "bun test", + "test:coverage": "bun test --coverage", + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.4", + "@types/bun": "latest", + "@project-cvsa/typescript-config": "workspace:*" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@better-auth/prisma-adapter": "^1.5.4", + "@elysiajs/eden": "^1.0.5", + "@prisma/adapter-pg": "^7.4.1", + "@project-cvsa/db": "workspace:*", + "@t3-oss/env-core": "^0.13.10", + "@project-cvsa/core": "workspace:*", + "argon2": "^0.44.0", + "better-auth": "^1.5.4", + "chalk": "^5.6.2", + "elysia": "^1.4.26", + "elysia-ip": "^1.0.10", + "elysia-rate-limit": "^4.5.0", + "nanoid": "^5.1.6", + "zod": "^4.3.6" + } } diff --git a/apps/backend/src/common/error/AppError.ts b/apps/backend/src/common/error/AppError.ts deleted file mode 100644 index 8ce3151..0000000 --- a/apps/backend/src/common/error/AppError.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class AppError extends Error { - constructor( - public override message: string, - public code: string, - public statusCode: number = 500, - public meta?: Record - ) { - super(message); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} diff --git a/apps/backend/src/common/error/ConflictError.ts b/apps/backend/src/common/error/ConflictError.ts index a81c370..75fbf65 100644 --- a/apps/backend/src/common/error/ConflictError.ts +++ b/apps/backend/src/common/error/ConflictError.ts @@ -1,4 +1,4 @@ -import { AppError } from "@common/error"; +import { AppError } from "@project-cvsa/core"; export class ConflictError extends AppError { constructor( diff --git a/apps/backend/src/common/error/RateLimitError.ts b/apps/backend/src/common/error/RateLimitError.ts index 6ea6e7a..91ec473 100644 --- a/apps/backend/src/common/error/RateLimitError.ts +++ b/apps/backend/src/common/error/RateLimitError.ts @@ -1,4 +1,4 @@ -import { AppError } from "@common/error"; +import { AppError } from "@project-cvsa/core"; export class RateLimitError extends AppError { constructor(message = "Request rate limit exceeded") { diff --git a/apps/backend/src/common/error/getErrorResponse.ts b/apps/backend/src/common/error/getErrorResponse.ts new file mode 100644 index 0000000..efa369b --- /dev/null +++ b/apps/backend/src/common/error/getErrorResponse.ts @@ -0,0 +1,12 @@ +import type { ErrorResponseDto } from "@project-cvsa/core"; + +// biome-ignore lint/suspicious/noExplicitAny: Utility type +type StatusFunc = (code: number, response: any) => any; + +export const getErrorResponse = ( + statusFunc: T, + statusCode: number, + data: ErrorResponseDto +): ReturnType => { + return statusFunc(statusCode, data); +}; diff --git a/apps/backend/src/common/error/index.ts b/apps/backend/src/common/error/index.ts index 4c04789..0bddc90 100644 --- a/apps/backend/src/common/error/index.ts +++ b/apps/backend/src/common/error/index.ts @@ -1,3 +1,3 @@ -export * from "./AppError"; export * from "./ConflictError"; export * from "./RateLimitError"; +export * from "./getErrorResponse"; diff --git a/apps/backend/src/common/middlewares/auth.ts b/apps/backend/src/common/middlewares/auth.ts index c0a9404..2a658f3 100644 --- a/apps/backend/src/common/middlewares/auth.ts +++ b/apps/backend/src/common/middlewares/auth.ts @@ -1,5 +1,5 @@ -import { AppError } from "@common/error"; -import { auth } from "@modules/auth/lib"; +import { AppError } from "@project-cvsa/core"; +import { auth } from "@project-cvsa/core"; import { Elysia } from "elysia"; import type { User } from "@project-cvsa/db"; @@ -16,11 +16,14 @@ const convertUser = (user: BetterAuthUser): User => { }; }; +const toBetterAuthHeaders = (headers: Record): [string, string][] => + Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== undefined); + export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( { as: "scoped" }, async ({ headers }) => { const session = await auth.api.getSession({ - headers: headers, + headers: toBetterAuthHeaders(headers), }); if (!session) { diff --git a/apps/backend/src/common/schemas/index.ts b/apps/backend/src/common/schemas/index.ts deleted file mode 100644 index 6df971a..0000000 --- a/apps/backend/src/common/schemas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ErrorResponseSchema"; diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index c197d24..9e05044 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -1,36 +1,38 @@ -import { AppError } from "@common/error"; +import { ZodError } from "zod"; import { APIError } from "better-auth"; import type { ErrorHandler } from "elysia"; -import { ZodError } from "zod"; +import { AppError } from "@project-cvsa/core"; +import { getErrorResponse } from "@/common/error"; export const errorHandler: ErrorHandler<{ readonly AppError: AppError; }> = ({ code, status, error }) => { + if (AppError.isAppError(error)) { + return getErrorResponse(status, error.statusCode, { + code: error.code, + message: error.message, + }); + } if (code === "NOT_FOUND") - return status(404, { + return getErrorResponse(status, 404, { + code: "NOT_FOUND", message: "The requested resource was not found.", }); if (code === "VALIDATION") { const detail = error.detail(error.message); if (typeof detail === "string") { - return status(422, { + return getErrorResponse(status, 422, { code: "VALIDATION_ERROR", message: detail, }); } - return status(422, { + return getErrorResponse(status, 422, { code: "VALIDATION_ERROR", message: detail.summary, }); } - if (error instanceof AppError) { - return status(error.statusCode, { - code: error.code, - message: error.message, - }); - } if (error instanceof ZodError) { - return status(422, { + return getErrorResponse(status, 422, { code: "VALIDATION_ERROR", message: error.message, }); @@ -41,18 +43,17 @@ export const errorHandler: ErrorHandler<{ error.body?.code || "" ) ) { - return status(409, { - code: error.body?.code, + return getErrorResponse(status, 409, { + code: error.body?.code || "ENTITY_CONFLICT", message: error.body?.message, }); } - return status(error.statusCode, { - code: error.body?.code, + return getErrorResponse(status, error.statusCode, { + code: error.body?.code || "AUTH_ERROR", message: error.body?.message, }); } - console.error(error); - return status(500, { + return getErrorResponse(status, 500, { code: "SERVER_ERROR", message: "Internal server error", }); diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 6ae631b..c224eff 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,8 +2,15 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler } from "@modules/index"; -import { AppError } from "@common/error"; +import { + authHandler, + songDetailsHandler, + songCreateHandler, + songUpdateHandler, + songDeleteHandler, + songListHandler, +} from "@modules/index"; +import { AppError } from "@project-cvsa/core"; import { errorHandler } from "./errorHandler"; const [host, port] = getBindingInfo(); @@ -20,6 +27,11 @@ export const app = new Elysia({ }) .onError(errorHandler) .use(authHandler) + .use(songDetailsHandler) + .use(songCreateHandler) + .use(songUpdateHandler) + .use(songDeleteHandler) + .use(songListHandler) .use(onAfterHandler) .listen(16412); diff --git a/apps/backend/src/modules/auth/routes/currentUser.ts b/apps/backend/src/modules/auth/currentUser.ts similarity index 50% rename from apps/backend/src/modules/auth/routes/currentUser.ts rename to apps/backend/src/modules/auth/currentUser.ts index 60281d0..dbe56f0 100644 --- a/apps/backend/src/modules/auth/routes/currentUser.ts +++ b/apps/backend/src/modules/auth/currentUser.ts @@ -1,19 +1,16 @@ import { Elysia } from "elysia"; -import { GetCurrentUserResponseSchema } from "@modules/auth/schema"; -import { ErrorResponseSchema } from "@common/schemas"; +import { + CurrentUserInfoSchema, + betterAuthToCurrentUserInfoDto, + ErrorResponseSchema, +} from "@project-cvsa/core"; import { authMiddleware } from "@common/middlewares"; export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( "/me", - async ({ status, user }) => { - return status(200, { - id: user.id, - username: user.username || "", - displayName: user.name, - email: user.email, - createdAt: user.createdAt, - image: user.image, - }); + async ({ status, session }) => { + const userInfo = betterAuthToCurrentUserInfoDto(session.user); + return status(200, userInfo); }, { detail: { @@ -21,7 +18,7 @@ export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( description: "", }, response: { - 200: GetCurrentUserResponseSchema, + 200: CurrentUserInfoSchema, 400: ErrorResponseSchema, 401: ErrorResponseSchema, 422: ErrorResponseSchema, diff --git a/apps/backend/src/modules/auth/index.ts b/apps/backend/src/modules/auth/index.ts index 9bf9b1b..77a1ee7 100644 --- a/apps/backend/src/modules/auth/index.ts +++ b/apps/backend/src/modules/auth/index.ts @@ -1 +1,5 @@ -export * from "./routes"; +import Elysia from "elysia"; +import { signupHandler } from "./signup"; +import { getCurrentUserHandler } from "./currentUser"; + +export const authHandler = new Elysia().use(signupHandler).use(getCurrentUserHandler); diff --git a/apps/backend/src/modules/auth/routes/index.ts b/apps/backend/src/modules/auth/routes/index.ts deleted file mode 100644 index 77a1ee7..0000000 --- a/apps/backend/src/modules/auth/routes/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Elysia from "elysia"; -import { signupHandler } from "./signup"; -import { getCurrentUserHandler } from "./currentUser"; - -export const authHandler = new Elysia().use(signupHandler).use(getCurrentUserHandler); diff --git a/apps/backend/src/modules/auth/schema/currentUser.ts b/apps/backend/src/modules/auth/schema/currentUser.ts deleted file mode 100644 index 3f1b140..0000000 --- a/apps/backend/src/modules/auth/schema/currentUser.ts +++ /dev/null @@ -1,10 +0,0 @@ -import z from "zod"; - -export const GetCurrentUserResponseSchema = z.object({ - id: z.string(), - username: z.string(), - displayName: z.string().optional().nullable(), - email: z.email().optional().nullable(), - createdAt: z.date(), - image: z.string().optional().nullable(), -}); diff --git a/apps/backend/src/modules/auth/schema/index.ts b/apps/backend/src/modules/auth/schema/index.ts deleted file mode 100644 index ace014d..0000000 --- a/apps/backend/src/modules/auth/schema/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./signup"; -export * from "./currentUser"; diff --git a/apps/backend/src/modules/auth/schema/signup.ts b/apps/backend/src/modules/auth/schema/signup.ts deleted file mode 100644 index cf25fec..0000000 --- a/apps/backend/src/modules/auth/schema/signup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import z from "zod"; - -export const SignupRequestSchema = z.object({ - username: z.string().min(1), - password: z.string().min(6), - displayName: z.string().max(100).optional().nullable(), - email: z.email().optional().nullable(), -}); - -export const SignupResponseSchema = z.object({ - message: z.string(), - data: z.object({ - id: z.string(), - username: z.string(), - displayName: z.string().optional().nullable(), - email: z.email().optional().nullable(), - token: z.string(), - }), -}); diff --git a/apps/backend/src/modules/auth/routes/signup.ts b/apps/backend/src/modules/auth/signup.ts similarity index 59% rename from apps/backend/src/modules/auth/routes/signup.ts rename to apps/backend/src/modules/auth/signup.ts index fa983d5..c088952 100644 --- a/apps/backend/src/modules/auth/routes/signup.ts +++ b/apps/backend/src/modules/auth/signup.ts @@ -1,11 +1,17 @@ import { Elysia } from "elysia"; import { ip } from "elysia-ip"; import { rateLimit } from "elysia-rate-limit"; -import { SignupRequestSchema, SignupResponseSchema } from "@modules/auth/schema"; -import { ErrorResponseSchema } from "@common/schemas"; -import { RateLimitError, AppError } from "@common/error"; -import { auth } from "@modules/auth/lib"; -import { getRandomId } from "@common/utils"; +import { + betterAuthToSignupUserInfoDto, + SignupRequestSchema, + signupRequestToBetterAuth, + SignupResponseSchema, + toSignUpResponse, + ErrorResponseSchema, +} from "@project-cvsa/core"; +import { RateLimitError } from "@common/error"; +import { AppError } from "@project-cvsa/core"; +import { auth } from "@project-cvsa/core"; const DAY = 86400; @@ -24,15 +30,12 @@ export const signupHandler = new Elysia() "/user", async ({ body, status, headers, cookie: { token: tokenCookie } }) => { const { user, token } = await auth.api.signUpEmail({ - body: { - name: body.displayName || body.username, - email: - body.email || `delegate-${getRandomId(14).toLowerCase()}@projectcvsa.com`, - password: body.password, - username: body.username, - }, - headers: headers, + body: signupRequestToBetterAuth(body), + headers: Object.entries(headers).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ), }); + if (!token) { throw new AppError("Cannot create user", "INTERNAL_SERVER_ERROR", 500, { cause: "Better Auth responded with no token", @@ -45,16 +48,10 @@ export const signupHandler = new Elysia() tokenCookie.secure = true; tokenCookie.sameSite = "lax"; - return status(200, { - message: "Successfully registered", - data: { - id: user.id, - username: user.username || "", - displayName: user.name, - email: user.email, - token, - }, - }); + const userInfo = betterAuthToSignupUserInfoDto(user, token); + const response = toSignUpResponse(userInfo); + + return status(200, response); }, { body: SignupRequestSchema, diff --git a/apps/backend/src/modules/catalog/song/create.ts b/apps/backend/src/modules/catalog/song/create.ts new file mode 100644 index 0000000..addd974 --- /dev/null +++ b/apps/backend/src/modules/catalog/song/create.ts @@ -0,0 +1,25 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { SongService, createSongSchema, ErrorResponseSchema } from "@project-cvsa/core"; +import { authMiddleware } from "@common/middlewares/auth"; + +const CreateSongRequestSchema = createSongSchema; + +export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( + "/song", + async ({ body, status }) => { + const song = await new SongService().create(body); + return status(201, song); + }, + { + body: CreateSongRequestSchema, + detail: { + summary: "Create Song", + description: "Create a new song (authentication required)", + }, + response: { + 201: z.object({}).passthrough(), + 401: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/catalog/song/delete.ts b/apps/backend/src/modules/catalog/song/delete.ts new file mode 100644 index 0000000..0715977 --- /dev/null +++ b/apps/backend/src/modules/catalog/song/delete.ts @@ -0,0 +1,31 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { SongService, ErrorResponseSchema } from "@project-cvsa/core"; +import { AppError } from "@project-cvsa/core"; +import { authMiddleware } from "@common/middlewares/auth"; + +export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) + .use(authMiddleware) + .delete( + "/song/:id", + async ({ params, set }) => { + const id = Number(params.id); + if (Number.isNaN(id)) { + throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); + } + await new SongService().delete(id); + set.status = 204; + }, + { + detail: { + summary: "Delete Song", + description: "Soft delete a song (authentication required)", + }, + response: { + 204: z.undefined(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/modules/catalog/song/details.ts b/apps/backend/src/modules/catalog/song/details.ts new file mode 100644 index 0000000..2a1e06e --- /dev/null +++ b/apps/backend/src/modules/catalog/song/details.ts @@ -0,0 +1,35 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { SongService, ErrorResponseSchema, type SongDetailsDto } from "@project-cvsa/core"; +import { AppError } from "@project-cvsa/core"; + +const SongDetailsResponseSchema: z.ZodType = z.any(); + +export const songDetailsHandler = new Elysia().get( + "/song/:id/details", + async ({ params, status }) => { + const id = Number(params.id); + try { + const song = await new SongService().getDetails(id); + return song; + } catch (e) { + if (e instanceof AppError && e.code === "NOT_FOUND") { + return status(404, { + code: e.code, + message: e.message, + }); + } + throw e; + } + }, + { + detail: { + summary: "Song Details", + description: "", + }, + response: { + 200: SongDetailsResponseSchema, + 404: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/catalog/song/index.ts b/apps/backend/src/modules/catalog/song/index.ts new file mode 100644 index 0000000..d5f6fc3 --- /dev/null +++ b/apps/backend/src/modules/catalog/song/index.ts @@ -0,0 +1,5 @@ +export * from "./create"; +export * from "./update"; +export * from "./delete"; +export * from "./list"; +export * from "./details"; diff --git a/apps/backend/src/modules/catalog/song/list.ts b/apps/backend/src/modules/catalog/song/list.ts new file mode 100644 index 0000000..a8af923 --- /dev/null +++ b/apps/backend/src/modules/catalog/song/list.ts @@ -0,0 +1,20 @@ +import { Elysia } from "elysia"; +import { SongService, ListSongsQuerySchema, ListSongsResponseSchema } from "@project-cvsa/core"; + +export const songListHandler = new Elysia({ name: "songListHandler" }).get( + "/song", + async ({ query }) => { + const result = await new SongService().list(query); + return result; + }, + { + query: ListSongsQuerySchema, + detail: { + summary: "List Songs", + description: "List songs with pagination (no authentication required)", + }, + response: { + 200: ListSongsResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/catalog/song/update.ts b/apps/backend/src/modules/catalog/song/update.ts new file mode 100644 index 0000000..5bec45d --- /dev/null +++ b/apps/backend/src/modules/catalog/song/update.ts @@ -0,0 +1,32 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { SongService, UpdateSongSchema, ErrorResponseSchema } from "@project-cvsa/core"; +import { AppError } from "@project-cvsa/core"; +import { authMiddleware } from "@common/middlewares/auth"; + +const UpdateSongRequestSchema = UpdateSongSchema; + +export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }).use(authMiddleware).put( + "/song/:id", + async ({ params, body }) => { + const id = Number(params.id); + if (Number.isNaN(id)) { + throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); + } + const song = await new SongService().update(id, body); + return song; + }, + { + body: UpdateSongRequestSchema, + detail: { + summary: "Update Song", + description: "Update an existing song (authentication required)", + }, + response: { + 200: z.object({}).passthrough(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/index.ts b/apps/backend/src/modules/index.ts index 97ccf76..e059185 100644 --- a/apps/backend/src/modules/index.ts +++ b/apps/backend/src/modules/index.ts @@ -1 +1,2 @@ export * from "./auth"; +export * from "./catalog/song"; diff --git a/apps/backend/tests/e2e/auth.test.ts b/apps/backend/tests/auth.test.ts similarity index 100% rename from apps/backend/tests/e2e/auth.test.ts rename to apps/backend/tests/auth.test.ts index 1977826..91721b8 100644 --- a/apps/backend/tests/e2e/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -12,9 +12,9 @@ describe("Registration E2E Tests - POST /v2/user", () => { }); afterAll(async () => { - await prisma.$disconnect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); + await prisma.$disconnect(); }); test("should register a new user", async () => { diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts new file mode 100644 index 0000000..c982e90 --- /dev/null +++ b/apps/backend/tests/song.test.ts @@ -0,0 +1,265 @@ +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@common/prisma"; + +const api = treaty(app); + +describe("Song E2E Tests", () => { + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.song.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.$disconnect(); + }); + + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: "test_user", + password: "password123", + email: "test@example.com", + }); + return signup.data?.data.token; + } + + describe("POST /v2/song - Create Song", () => { + test("should create a song with authentication", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Test Song", + type: "ORIGINAL" as const, + duration: 180, + }; + + const { data, status } = await api.v2.song.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + }); + + test("should return 401 without authentication", async () => { + const payload = { + name: "Test Song", + }; + + const { error, status } = await api.v2.song.post(payload); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + }); + + describe("PUT /v2/song/:id - Update Song", () => { + test("should update a song with authentication", async () => { + const token = await getAuthToken(); + + const song = await prisma.song.create({ + data: { + name: "Original Name", + }, + }); + + const { data, status } = await api.v2.song({ id: song.id }).put( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.name).toBe("Updated Name"); + }); + + test("should return 401 without authentication", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + }, + }); + + const { status } = await api.v2.song({ id: song.id }).put({ + name: "Updated Name", + }); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent song", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2.song({ id: 999999 }).put( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("DELETE /v2/song/:id - Delete Song", () => { + test("should soft delete a song with authentication", async () => { + const token = await getAuthToken(); + + const song = await prisma.song.create({ + data: { + name: "Song to Delete", + }, + }); + + const { status } = await api.v2.song({ id: song.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedSong = await prisma.song.findUnique({ + where: { id: song.id }, + }); + expect(deletedSong?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + }, + }); + + const { status } = await api.v2.song({ id: song.id }).delete({}, {}); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent song", async () => { + const token = await getAuthToken(); + + const { status } = await api.v2.song({ id: 999999 }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + }); + }); + + describe("GET /v2/song - List Songs", () => { + test("should list songs without authentication", async () => { + await prisma.song.create({ + data: { name: "Song 1" }, + }); + await prisma.song.create({ + data: { name: "Song 2" }, + }); + + const { data, status } = await api.v2.song.get(); + + expect(status).toBe(200); + expect(data?.songs).toHaveLength(2); + expect(data?.total).toBe(2); + }); + + test("should support pagination", async () => { + for (let i = 1; i <= 5; i++) { + await prisma.song.create({ + data: { name: `Song ${i}` }, + }); + } + + const { data, status } = await api.v2.song.get({ + query: { offset: 0, limit: 2 }, + }); + + expect(status).toBe(200); + expect(data?.songs).toHaveLength(2); + expect(data?.total).toBe(5); + }); + + test("should filter by search term", async () => { + await prisma.song.create({ + data: { name: "Unique Song Name" }, + }); + await prisma.song.create({ + data: { name: "Another Song" }, + }); + + const { data, status } = await api.v2.song.get({ + query: { search: "Unique" }, + }); + + expect(status).toBe(200); + expect(data?.songs).toHaveLength(1); + expect(data?.songs[0]?.name).toBe("Unique Song Name"); + }); + + test("should filter by type", async () => { + await prisma.song.create({ + data: { name: "Original Song", type: "ORIGINAL" }, + }); + await prisma.song.create({ + data: { name: "Cover Song", type: "COVER" }, + }); + + const { data, status } = await api.v2.song.get({ + query: { type: "ORIGINAL" }, + }); + + expect(status).toBe(200); + expect(data?.songs).toHaveLength(1); + expect(data?.songs[0]?.type).toBe("ORIGINAL"); + }); + + test("should exclude soft-deleted songs", async () => { + await prisma.song.create({ + data: { name: "Active Song" }, + }); + const deletedSong = await prisma.song.create({ + data: { name: "Deleted Song" }, + }); + await prisma.song.update({ + where: { id: deletedSong.id }, + data: { deletedAt: new Date() }, + }); + + const { data, status } = await api.v2.song.get(); + + expect(status).toBe(200); + expect(data?.songs).toHaveLength(1); + expect(data?.songs[0]?.name).toBe("Active Song"); + }); + }); +}); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index f2c10c3..48b006c 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,22 +1,13 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": ["ESNext"], - "module": "preserve", - "allowImportingTsExtensions": true, - "noEmit": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "bundler", - "isolatedModules": true, - "incremental": true, - "resolveJsonModule": true, - "paths": { - "@common/*": ["./src/common/*"], - "@modules/*": ["./src/modules/*"], - "@lib/*": ["./src/lib/*"], - "@/*": ["./src/*"] - } - } + "extends": "@project-cvsa/typescript-config/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@modules/*": ["./src/modules/*"], + "@common/*": ["./src/common/*"] + } + }, + "include": ["./src/**/*", "./tests/**/*"], + "exclude": ["../../packages/**/*"] } diff --git a/biome.json b/biome.json index f53f194..c9b04b4 100644 --- a/biome.json +++ b/biome.json @@ -1,43 +1,47 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", - "assist": { - "actions": { - "source": { - "useSortedKeys": "off", - "organizeImports": "off" - } - }, - "enabled": true - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "indentWidth": 4, - "lineWidth": 100 - }, - "javascript": { - "formatter": { - "enabled": true, - "indentStyle": "tab", - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "es5" - } - }, - "json": { - "assist": { - "enabled": false - }, - "formatter": { - "enabled": false - }, - "linter": { - "enabled": false - } - }, - "vcs": { - "clientKind": "git", - "enabled": true, - "useIgnoreFile": true - } + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": "off", + "organizeImports": "off" + } + }, + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 4, + "lineWidth": 100, + "formatWithErrors": true + }, + "javascript": { + "formatter": { + "enabled": true, + "indentStyle": "tab", + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "json": { + "assist": { + "enabled": false + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": false + } + }, + "vcs": { + "clientKind": "git", + "enabled": true, + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*", "!.zed/*", "!packages/typescript-config/base.json"] + } } diff --git a/bun.lock b/bun.lock index 0f875fa..4861b90 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,8 @@ "": { "name": "@project-cvsa/backend", "devDependencies": { - "turbo": "latest", + "@biomejs/biome": "2.3.8", + "turbo": "^2.8.16", }, }, "apps/backend": { @@ -15,7 +16,7 @@ "@better-auth/prisma-adapter": "^1.5.4", "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/configs": "workspace:*", + "@project-cvsa/core": "workspace:*", "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", @@ -29,14 +30,31 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", + "@project-cvsa/typescript-config": "*", "@types/bun": "latest", }, "peerDependencies": { "typescript": "^5", }, }, - "packages/config": { - "name": "@project-cvsa/configs", + "packages/core": { + "name": "@project-cvsa/core", + "dependencies": { + "@better-auth/prisma-adapter": "^1.5.5", + "@project-cvsa/db": "*", + "@t3-oss/env-core": "^0.13.10", + "better-auth": "^1.5.5", + "nanoid": "^5.1.6", + "zod": "^4.3.6", + }, + "devDependencies": { + "@prisma/adapter-pg": "^7.4.1", + "@project-cvsa/typescript-config": "*", + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, }, "packages/db": { "name": "@project-cvsa/db", @@ -46,6 +64,7 @@ "@types/pg": "^8.16.0", }, "devDependencies": { + "@project-cvsa/typescript-config": "*", "prisma": "^7.4.1", "prisma-json-types-generator": "^4.1.1", }, @@ -53,45 +72,48 @@ "typescript": "^5", }, }, + "packages/typescript-config": { + "name": "@project-cvsa/typescript-config", + }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], + "@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-4M4nMAWrDd3TmpV6dONkJjybBVKRZghe5Oj0NNyDEoXubxastQdO7Sb5B54I1rTx5yoMgsqaB+kbJnu/9UgjQg=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-DPww7rIfz6Ed7dZlJSW9xMQ42VKaJLB5Cs+pPqd+UHKRyighKjf3VgvMIcAdFPc4olQ0qRHo3+ZJhFlBCxRhxA=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0" } }, "sha512-iiWYut9rbQqiAsgRBtj6+nxanwjapxRgpIJbiS2o81h7b9iclE0AiDA0Foes590gdFQvskNauZcCpuF8ytxthg=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0" } }, "sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-ArzJN5Obk6i6+vLK1HpPzLIcsjxZYXPPUvxVU8eyU5HyoUT2MlswWfPQ8UJAKPn0iq/T4PVp/wZcQMhWk1tuNA=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ZQTbcBopw/ezjjbNFsfR3CRp0QciC4tJCarAnB5G9fZtUYbDjfY0vZOxIRmU4kI3x755CXQpGqTrkwmXaMRa3w=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.4", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.4" } }, "sha512-mGXTY7Ecxo7uvlMr6TFCBUvlH0NUMOeE9LKgPhG4HyhBN6VfCEg/DD9PG0Z2IatmMWQbckkt7ox5A0eBpG9m5w=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], @@ -159,10 +181,12 @@ "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], - "@project-cvsa/configs": ["@project-cvsa/configs@workspace:packages/config"], + "@project-cvsa/core": ["@project-cvsa/core@workspace:packages/core"], "@project-cvsa/db": ["@project-cvsa/db@workspace:packages/db"], + "@project-cvsa/typescript-config": ["@project-cvsa/typescript-config@workspace:packages/typescript-config"], + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -189,7 +213,7 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "better-auth": ["better-auth@1.5.4", "", { "dependencies": { "@better-auth/core": "1.5.4", "@better-auth/drizzle-adapter": "1.5.4", "@better-auth/kysely-adapter": "1.5.4", "@better-auth/memory-adapter": "1.5.4", "@better-auth/mongo-adapter": "1.5.4", "@better-auth/prisma-adapter": "1.5.4", "@better-auth/telemetry": "1.5.4", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-ReykcEKx6Kp9560jG1wtlDBnftA7L7xb3ZZdDWm5yGXKKe2pUf+oBjH0fqekrkRII0m4XBVQbQ0mOrFv+3FdYg=="], + "better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], @@ -419,19 +443,19 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.8.14", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.14", "turbo-darwin-arm64": "2.8.14", "turbo-linux-64": "2.8.14", "turbo-linux-arm64": "2.8.14", "turbo-windows-64": "2.8.14", "turbo-windows-arm64": "2.8.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-UCTxeMNYT1cKaHiIFdLCQ7ulI+jw5i5uOnJOrRXsgUD7G3+OjlUjwVd7JfeVt2McWSVGjYA3EVW/v1FSsJ5DtA=="], + "turbo": ["turbo@2.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-9sFi7n2lLfEsGWi5OEoA/eTtQU2BPKtzSYKqufMtDeRmqMT9vKjbv9gJCRkllSVE9BOXA0qXC3diyX8V8rKIKw=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aS4yJuy6A1PCLws+PJpZP0qCURG8Y5iVx13z/WAbKyeDTY6W6PiGgcEllSaeLGxyn++382ztN/EZH85n2zZ6VQ=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="], - "turbo-linux-64": ["turbo-linux-64@2.8.14", "", { "os": "linux", "cpu": "x64" }, "sha512-XC6wPUDJkakjhNLaS0NrHDMiujRVjH+naEAwvKLArgqRaFkNxjmyNDRM4eu3soMMFmjym6NTxYaF74rvET+Orw=="], + "turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ChfE7isyVNjZrVSPDwcfqcHLG/FuIBbOFxnt1FM8vSuBGzHAs8AlTdwFNIxlEMJfZ8Ad9mdMxdmsCUPIWiQ6cg=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="], - "turbo-windows-64": ["turbo-windows-64@2.8.14", "", { "os": "win32", "cpu": "x64" }, "sha512-FTbIeQL1ycLFW2t9uQNMy+bRSzi3Xhwun/e7ZhFBdM+U0VZxxrtfYEBM9CHOejlfqomk6Jh7aRz0sJoqYn39Hg=="], + "turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-KgZX12cTyhY030qS7ieT8zRkhZZE2VWJasDFVUSVVn17nR7IShpv68/7j5UqJNeRLIGF1XPK0phsP5V5yw3how=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -459,6 +483,8 @@ "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "@project-cvsa/backend/@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], @@ -467,6 +493,22 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + + "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], } } diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..48c1311 --- /dev/null +++ b/opencode.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "bash": { + "git *": "ask", + "jj *": "allow", + "bun run test": "allow", + "bun typecheck": "allow", + "bun dev": "deny", + "bun run dev": "deny", + "bun lint": "allow" + } + } +} diff --git a/package.json b/package.json index 2a72244..e891c29 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,23 @@ { - "name": "cvsa", - "private": true, - "workspaces": [ - "apps/*", - "packages/*" - ], - "scripts": { - "dev": "bunx --bun turbo run dev", - "build": "bunx --bun turbo run build", - "lint": "biome check .", - "lint:fix": "biome check --write --unsafe .", - "format": "biome format --write .", - "test": "bunx --bun turbo run test" - }, - "devDependencies": { - "turbo": "latest" - }, - "packageManager": "bun@1.3.10" + "name": "cvsa", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "bunx --bun turbo run dev", + "build": "bunx --bun turbo run build", + "lint": "biome check .", + "lint:fix": "biome check --write --unsafe .", + "format": "biome format --write .", + "test": "bunx --bun turbo run test", + "test:coverage": "bunx --bun turbo run test:coverage", + "typecheck": "bunx --bun turbo typecheck" + }, + "devDependencies": { + "@biomejs/biome": "2.3.8", + "turbo": "^2.8.16" + }, + "packageManager": "bun@1.3.10" } diff --git a/packages/core/.env.example b/packages/core/.env.example new file mode 100644 index 0000000..701c7b7 --- /dev/null +++ b/packages/core/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file diff --git a/packages/core/common/env.ts b/packages/core/common/env.ts new file mode 100644 index 0000000..823f926 --- /dev/null +++ b/packages/core/common/env.ts @@ -0,0 +1,13 @@ +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + DATABASE_URL: z.url(), + NODE_ENV: z.enum(["development", "test", "production"]).optional(), + }, + + runtimeEnv: import.meta.env, + + emptyStringAsUndefined: true, +}); diff --git a/packages/core/common/error/AppError.ts b/packages/core/common/error/AppError.ts new file mode 100644 index 0000000..1ec5ab1 --- /dev/null +++ b/packages/core/common/error/AppError.ts @@ -0,0 +1,28 @@ +export const APP_ERROR_MAGIC_NUMBER = 0x0712_0412_1099; + +export class AppError extends Error { + private readonly __type = APP_ERROR_MAGIC_NUMBER; + + constructor( + public override message: string, + public code: string, + public statusCode: number = 500, + public meta?: Record + ) { + super(message); + this.name = new.target.name; + Error.captureStackTrace(this, new.target); + } + + static isAppError(obj: unknown): obj is AppError { + if (!obj || typeof obj !== 'object') { + return false; + } + + if (obj instanceof AppError) { + return true; + } + + return '__type' in obj && (obj as { __type: unknown }).__type === APP_ERROR_MAGIC_NUMBER; + } +} diff --git a/packages/core/common/error/index.ts b/packages/core/common/error/index.ts new file mode 100644 index 0000000..1777091 --- /dev/null +++ b/packages/core/common/error/index.ts @@ -0,0 +1 @@ +export * from "./AppError"; diff --git a/packages/core/common/index.ts b/packages/core/common/index.ts new file mode 100644 index 0000000..a3f7d7e --- /dev/null +++ b/packages/core/common/index.ts @@ -0,0 +1,2 @@ +export * from "./schema"; +export * from "./error"; diff --git a/packages/core/common/internal.ts b/packages/core/common/internal.ts new file mode 100644 index 0000000..fb1846e --- /dev/null +++ b/packages/core/common/internal.ts @@ -0,0 +1,9 @@ +/** + * Internal entry point for the core package. + * * Provides access to internal members and objects from `@project-cvsa/core/common` + * that are intended for internal use only and are not part of the public API. + */ + +export * from "./env"; +export * from "./utils"; +export * from "./prisma"; diff --git a/packages/core/common/prisma.ts b/packages/core/common/prisma.ts new file mode 100644 index 0000000..2a0f438 --- /dev/null +++ b/packages/core/common/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from "@project-cvsa/db"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { env } from "@project-cvsa/core/common"; + +const adapter = new PrismaPg({ + connectionString: env.DATABASE_URL, +}); + +export const prisma = new PrismaClient({ adapter }); diff --git a/apps/backend/src/common/schemas/ErrorResponseSchema.ts b/packages/core/common/schema/error.ts similarity index 55% rename from apps/backend/src/common/schemas/ErrorResponseSchema.ts rename to packages/core/common/schema/error.ts index 0489581..e5d632e 100644 --- a/apps/backend/src/common/schemas/ErrorResponseSchema.ts +++ b/packages/core/common/schema/error.ts @@ -1,6 +1,8 @@ import { z } from "zod"; export const ErrorResponseSchema = z.object({ - code: z.string().optional(), + code: z.string(), message: z.string().optional(), }); + +export type ErrorResponseDto = z.infer; diff --git a/packages/core/common/schema/index.ts b/packages/core/common/schema/index.ts new file mode 100644 index 0000000..0ad13d9 --- /dev/null +++ b/packages/core/common/schema/index.ts @@ -0,0 +1 @@ +export * from "./error"; diff --git a/apps/backend/src/common/utils/index.ts b/packages/core/common/utils/index.ts similarity index 100% rename from apps/backend/src/common/utils/index.ts rename to packages/core/common/utils/index.ts diff --git a/apps/backend/src/common/utils/randomId.ts b/packages/core/common/utils/randomId.ts similarity index 100% rename from apps/backend/src/common/utils/randomId.ts rename to packages/core/common/utils/randomId.ts diff --git a/packages/core/index.ts b/packages/core/index.ts new file mode 100644 index 0000000..c0e36b9 --- /dev/null +++ b/packages/core/index.ts @@ -0,0 +1,2 @@ +export * from "./common"; +export * from "./modules"; diff --git a/apps/backend/src/modules/auth/lib/index.ts b/packages/core/modules/auth/betterAuth.ts similarity index 83% rename from apps/backend/src/modules/auth/lib/index.ts rename to packages/core/modules/auth/betterAuth.ts index 117eb92..f8ee110 100644 --- a/apps/backend/src/modules/auth/lib/index.ts +++ b/packages/core/modules/auth/betterAuth.ts @@ -1,8 +1,8 @@ -import { betterAuth } from "better-auth"; +import { APIError, betterAuth } from "better-auth"; import { prismaAdapter } from "@better-auth/prisma-adapter"; -import { prisma } from "@common/prisma"; +import { prisma } from "@project-cvsa/core/common"; +import { getRandomId } from "@project-cvsa/core/common"; import { username } from "better-auth/plugins"; -import { getRandomId } from "@common/utils/randomId"; import { bearer } from "better-auth/plugins"; const DAY = 24 * 60 * 60; @@ -45,3 +45,5 @@ export const auth = betterAuth({ }, plugins: [username(), bearer()], }); + +export { APIError as BetterAuthAPIError }; diff --git a/packages/core/modules/auth/index.ts b/packages/core/modules/auth/index.ts new file mode 100644 index 0000000..efe2ded --- /dev/null +++ b/packages/core/modules/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./betterAuth"; +export * from "./user"; diff --git a/packages/core/modules/auth/user/dto.ts b/packages/core/modules/auth/user/dto.ts new file mode 100644 index 0000000..f4e9a06 --- /dev/null +++ b/packages/core/modules/auth/user/dto.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; +import type { auth } from "@project-cvsa/core"; +import { getRandomId } from "@project-cvsa/core/common"; + +type BetterAuthUser = Exclude>, null>["user"]; + +type BetterAuthSignupRequestBody = Exclude< + Parameters[0], + undefined +>["body"]; + +export const SignupRequestSchema = z.object({ + username: z.string().min(1), + password: z.string().min(8), + displayName: z.string().max(100).optional().nullable(), + email: z.email().optional().nullable(), +}); + +export const SignupUserInfoSchema = z.object({ + id: z.string(), + username: z.string(), + displayName: z.string().optional().nullable(), + email: z.email().optional().nullable(), + token: z.string(), +}); + +export const CurrentUserInfoSchema = z.object({ + id: z.string(), + username: z.string(), + displayName: z.string().optional().nullable(), + email: z.email().optional().nullable(), + createdAt: z.date().optional().nullable(), + image: z.string().optional().nullable(), +}); + +export const SignupResponseSchema = z.object({ + message: z.string(), + data: SignupUserInfoSchema, +}); + +export type SignupRequestDto = z.infer; +export type SignupUserInfoDto = z.infer; +export type CurrentUserInfoDto = z.infer; +export type SignupResponseDto = z.infer; + +export function signupRequestToBetterAuth(body: SignupRequestDto): BetterAuthSignupRequestBody { + const fakeEmail = `delegate-${getRandomId(14).toLowerCase()}@projectcvsa.com`; + + return { + name: body.displayName || body.username, + email: body.email || fakeEmail, + password: body.password, + username: body.username, + }; +} + +export function betterAuthToSignupUserInfoDto( + user: BetterAuthUser, + token: string +): SignupUserInfoDto { + return { + id: user.id, + username: user.username ?? "", + displayName: user.name, + email: user.email, + token, + }; +} + +export function betterAuthToCurrentUserInfoDto(user: BetterAuthUser): CurrentUserInfoDto { + return { + id: user.id, + username: user.username ?? "", + displayName: user.name, + email: user.email, + createdAt: user.createdAt ?? null, + image: user.image ?? null, + }; +} + +export function toSignUpResponse(data: SignupUserInfoDto): SignupResponseDto { + return { + message: "Successfully registered", + data, + }; +} diff --git a/packages/core/modules/auth/user/index.ts b/packages/core/modules/auth/user/index.ts new file mode 100644 index 0000000..0392b1b --- /dev/null +++ b/packages/core/modules/auth/user/index.ts @@ -0,0 +1 @@ +export * from "./dto"; diff --git a/packages/core/modules/catalog/index.ts b/packages/core/modules/catalog/index.ts new file mode 100644 index 0000000..153b6ce --- /dev/null +++ b/packages/core/modules/catalog/index.ts @@ -0,0 +1 @@ +export * from "./song"; diff --git a/packages/core/modules/catalog/song/dto.ts b/packages/core/modules/catalog/song/dto.ts new file mode 100644 index 0000000..4c511c5 --- /dev/null +++ b/packages/core/modules/catalog/song/dto.ts @@ -0,0 +1,97 @@ +import { z } from "zod"; +import type { Song, Singer, Artist, ArtistRole } from "@project-cvsa/db"; + +export const SongTypeSchema = z.enum([ + "ORIGINAL", + "COVER", + "REMIX", + "REMASTER", + "MASHUP", + "INSTRUMENTAL", + "OTHERS", +]); + +export type SongId = number; + +// Performance DTOs +export const CreatePerformanceSchema = z.object({ + singerId: z.number().int().positive(), + voicebankId: z.number().int().positive().nullable().optional(), + svsEngineId: z.number().int().positive().nullable().optional(), + svsEngineVersionId: z.number().int().positive().nullable().optional(), +}); + +export type CreatePerformanceDto = z.infer; + +// Creation DTOs +export const CreateCreationSchema = z.object({ + artistId: z.number().int().positive(), + roleId: z.number().int().positive(), +}); + +export type CreateCreationDto = z.infer; + +// Song with relations DTOs +export const createSongSchema = z.object({ + type: SongTypeSchema.nullable().optional(), + name: z.string().nullable().optional(), + duration: z.number().nullable().optional(), + description: z.string().nullable().optional(), + coverUrl: z.string().url().nullable().optional(), + publishedAt: z.date().nullable().optional(), + performances: z.array(CreatePerformanceSchema).optional(), + creations: z.array(CreateCreationSchema).optional(), +}); + +export type CreateSongDto = z.infer; + +export const UpdateSongSchema = z.object({ + type: SongTypeSchema.nullable().optional(), + name: z.string().nullable().optional(), + duration: z.number().nullable().optional(), + description: z.string().nullable().optional(), + coverUrl: z.string().url().nullable().optional(), + publishedAt: z.date().nullable().optional(), + deletedAt: z.date().nullable().optional(), + performances: z.array(CreatePerformanceSchema).optional(), + creations: z.array(CreateCreationSchema).optional(), +}); + +export type UpdateSongDto = z.infer; + +export const ListSongsQuerySchema = z.object({ + search: z.string().optional(), + type: SongTypeSchema.optional(), + offset: z.coerce.number().int().min(0).optional(), + limit: z.coerce.number().int().min(1).max(100).optional(), +}); + +export type ListSongsQueryDto = z.infer; + +export const ListSongsResponseSchema = z.object({ + songs: z.array( + z.object({ + id: z.number(), + type: SongTypeSchema.nullable(), + name: z.string().nullable(), + duration: z.number().nullable(), + description: z.string().nullable(), + coverUrl: z.string().url().nullable(), + publishedAt: z.date().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + }) + ), + total: z.number(), + offset: z.number(), + limit: z.number(), +}); + +export type ListSongsResponseDto = z.infer; + +export type SongDetailsDto = Song & { + singers: Singer[]; + artists: (Artist & { + role: ArtistRole; + })[]; +}; diff --git a/packages/core/modules/catalog/song/index.ts b/packages/core/modules/catalog/song/index.ts new file mode 100644 index 0000000..d0cdb18 --- /dev/null +++ b/packages/core/modules/catalog/song/index.ts @@ -0,0 +1,3 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./service"; diff --git a/packages/core/modules/catalog/song/repository.interface.ts b/packages/core/modules/catalog/song/repository.interface.ts new file mode 100644 index 0000000..292e8da --- /dev/null +++ b/packages/core/modules/catalog/song/repository.interface.ts @@ -0,0 +1,14 @@ +import type { Song } from "@project-cvsa/db"; +import type { SongDetailsDto } from "./dto"; +import type { CreateSongDto, ListSongsQueryDto, SongId, UpdateSongDto } from "./dto"; + +export interface ISongRepository { + getById(id: SongId): Promise; + getDetailsById(id: SongId): Promise; + list(query?: ListSongsQueryDto): Promise<{ songs: Song[]; total: number }>; + create(input: CreateSongDto): Promise; + createWithRelations(input: CreateSongDto): Promise; + update(id: SongId, input: UpdateSongDto): Promise; + updateWithRelations(id: SongId, input: UpdateSongDto): Promise; + softDelete(id: SongId): Promise; +} diff --git a/packages/core/modules/catalog/song/repository.ts b/packages/core/modules/catalog/song/repository.ts new file mode 100644 index 0000000..96910fb --- /dev/null +++ b/packages/core/modules/catalog/song/repository.ts @@ -0,0 +1,179 @@ +import type { Prisma, PrismaClient } from "@project-cvsa/db"; +import type { SongDetailsDto } from "./dto"; +import type { CreateSongDto, ListSongsQueryDto, SongId, UpdateSongDto } from "./dto"; +import type { ISongRepository } from "./repository.interface"; + +export class SongRepository implements ISongRepository { + constructor(private readonly prisma: PrismaClient) {} + + async getById(id: SongId) { + return this.prisma.song.findFirst({ where: { id, deletedAt: null } }); + } + + async getDetailsById(id: SongId): Promise { + const data = await this.prisma.song.findFirst({ + where: { id, deletedAt: null }, + include: { + performances: { + include: { + singer: true, + }, + }, + creations: { + include: { + role: true, + artist: true, + }, + }, + }, + }); + if (!data) return null; + const { performances, creations, ...song } = data; + return { + singers: performances.map((item) => item.singer), + artists: creations.map((item) => { + return { + ...item.artist, + role: item.role, + }; + }), + ...song, + }; + } + + async list(query: ListSongsQueryDto = {}) { + const { offset: skip = 0, limit: take = 50, type, search } = query; + + const where: Prisma.SongWhereInput = { + deletedAt: null, + type, + }; + + if (search) { + where.OR = [ + { name: { contains: search, mode: "insensitive" } }, + { description: { contains: search, mode: "insensitive" } }, + ]; + } + + const [songs, total] = await Promise.all([ + this.prisma.song.findMany({ + where, + skip, + take, + orderBy: { createdAt: "desc" }, + }), + this.prisma.song.count({ where }), + ]); + + return { songs, total }; + } + + async create(input: CreateSongDto) { + return await this.prisma.song.create({ + data: { + type: input.type ?? null, + name: input.name ?? null, + duration: input.duration ?? null, + description: input.description ?? null, + coverUrl: input.coverUrl ?? null, + publishedAt: input.publishedAt ?? null, + }, + }); + } + + async createWithRelations(input: CreateSongDto) { + const { performances, creations, ...songData } = input; + + return this.prisma.song.create({ + data: { + type: songData.type ?? null, + name: songData.name ?? null, + duration: songData.duration ?? null, + description: songData.description ?? null, + coverUrl: songData.coverUrl ?? null, + publishedAt: songData.publishedAt ?? null, + performances: performances + ? { + create: performances.map((p) => ({ + singerId: p.singerId, + voicebankId: p.voicebankId ?? undefined, + svsEngineId: p.svsEngineId ?? undefined, + svsEngineVersionId: p.svsEngineVersionId ?? undefined, + })), + } + : undefined, + creations: creations + ? { + create: creations.map((c) => ({ + artistId: c.artistId, + roleId: c.roleId, + artistRoleId: c.roleId, + })), + } + : undefined, + }, + }); + } + + async update(id: SongId, input: UpdateSongDto) { + return this.prisma.song.update({ + where: { id }, + data: { + ...(input.type !== undefined ? { type: input.type } : {}), + ...(input.name !== undefined ? { name: input.name } : {}), + ...(input.duration !== undefined ? { duration: input.duration } : {}), + ...(input.description !== undefined ? { description: input.description } : {}), + ...(input.coverUrl !== undefined ? { coverUrl: input.coverUrl } : {}), + ...(input.publishedAt !== undefined ? { publishedAt: input.publishedAt } : {}), + ...(input.deletedAt !== undefined ? { deletedAt: input.deletedAt } : {}), + }, + }); + } + + async updateWithRelations(id: SongId, input: UpdateSongDto) { + const { performances, creations, ...songData } = input; + + const data: Prisma.SongUpdateInput = { + ...(songData.type !== undefined ? { type: songData.type } : {}), + ...(songData.name !== undefined ? { name: songData.name } : {}), + ...(songData.duration !== undefined ? { duration: songData.duration } : {}), + ...(songData.description !== undefined ? { description: songData.description } : {}), + ...(songData.coverUrl !== undefined ? { coverUrl: songData.coverUrl } : {}), + ...(songData.publishedAt !== undefined ? { publishedAt: songData.publishedAt } : {}), + ...(songData.deletedAt !== undefined ? { deletedAt: songData.deletedAt } : {}), + }; + + if (performances !== undefined) { + data.performances = { + deleteMany: {}, + create: performances.map((p) => ({ + singerId: p.singerId, + voicebankId: p.voicebankId ?? undefined, + svsEngineId: p.svsEngineId ?? undefined, + svsEngineVersionId: p.svsEngineVersionId ?? undefined, + })), + }; + } + + if (creations !== undefined) { + data.creations = { + deleteMany: {}, + create: creations.map((c) => ({ + artistId: c.artistId, + roleId: c.roleId, + artistRoleId: c.roleId, + })), + }; + } + + return this.prisma.song.update({ + where: { id }, + data, + }); + } + + async softDelete(id: SongId) { + await this.prisma.song.update({ where: { id }, data: { deletedAt: new Date() } }); + } +} diff --git a/packages/core/modules/catalog/song/service.ts b/packages/core/modules/catalog/song/service.ts new file mode 100644 index 0000000..8e60d79 --- /dev/null +++ b/packages/core/modules/catalog/song/service.ts @@ -0,0 +1,49 @@ +import { prisma } from "@project-cvsa/core/common"; +import type { SongDetailsDto, SongId, ListSongsQueryDto, ListSongsResponseDto } from "./dto"; +import type { CreateSongDto, UpdateSongDto } from "./dto"; +import { SongRepository } from "./repository"; +import { AppError } from "@project-cvsa/core"; +import type { Song } from "@project-cvsa/db"; + +export class SongService { + constructor(private readonly repository: SongRepository = new SongRepository(prisma)) {} + + async getDetails(id: SongId): Promise { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("Song not found", "NOT_FOUND", 404); + } + return result; + } + + async create(input: CreateSongDto): Promise { + return this.repository.createWithRelations(input); + } + + async update(id: SongId, input: UpdateSongDto): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("Song not found", "NOT_FOUND", 404); + } + return this.repository.updateWithRelations(id, input); + } + + async delete(id: SongId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("Song not found", "NOT_FOUND", 404); + } + await this.repository.softDelete(id); + } + + async list(query: ListSongsQueryDto = {}): Promise { + const { offset = 0, limit = 50 } = query; + const result = await this.repository.list(query); + return { + songs: result.songs, + total: result.total, + offset, + limit, + }; + } +} diff --git a/packages/core/modules/index.ts b/packages/core/modules/index.ts new file mode 100644 index 0000000..25b4339 --- /dev/null +++ b/packages/core/modules/index.ts @@ -0,0 +1,2 @@ +export * from "./auth"; +export * from "./catalog"; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..92fb061 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,32 @@ +{ + "name": "@project-cvsa/core", + "type": "module", + "module": "index.ts", + "scripts": { + "test": "bun test", + "test:coverage": "bun test --coverage", + "test:unit": "bun test tests/unit", + "test:integration": "bun test tests/integration", + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@prisma/adapter-pg": "^7.4.1", + "@types/bun": "latest", + "@project-cvsa/typescript-config": "*" + }, + "peerDependencies": { + "typescript": "^5" + }, + "exports": { + ".": "./index.ts", + "./common": "./common/internal.ts" + }, + "dependencies": { + "@better-auth/prisma-adapter": "^1.5.5", + "@project-cvsa/db": "*", + "@t3-oss/env-core": "^0.13.10", + "better-auth": "^1.5.5", + "nanoid": "^5.1.6", + "zod": "^4.3.6" + } +} diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts new file mode 100644 index 0000000..02004bd --- /dev/null +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -0,0 +1,190 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { PrismaClient } from "@project-cvsa/db"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { SongRepository } from "@project-cvsa/core"; +import type { CreateSongDto, UpdateSongDto } from "@project-cvsa/core"; +import { env } from "@project-cvsa/core/common"; + +const prisma = new PrismaClient({ + adapter: new PrismaPg({ + connectionString: env.DATABASE_URL, + }), +}); + +const repository = new SongRepository(prisma); + +describe("SongRepository Integration Tests", () => { + beforeAll(async () => { + await prisma.$connect(); + }); + + afterAll(async () => { + await prisma.song.deleteMany({ where: { deletedAt: null } }); + await prisma.$disconnect(); + }); + + beforeEach(async () => { + await prisma.song.deleteMany({ where: { deletedAt: null } }); + }); + + describe("create", () => { + test("should create a song with all fields", async () => { + const input: CreateSongDto = { + type: "ORIGINAL", + name: "Test Song", + duration: 180, + description: "A test song", + coverUrl: "https://example.com/cover.jpg", + publishedAt: new Date("2024-01-01"), + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.type).toBe("ORIGINAL"); + expect(result.name).toBe("Test Song"); + expect(result.duration).toBe(180); + expect(result.description).toBe("A test song"); + expect(result.coverUrl).toBe("https://example.com/cover.jpg"); + expect(result.deletedAt).toBeNull(); + }); + + test("should create a song with minimal fields", async () => { + const input: CreateSongDto = { + name: "Minimal Song", + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("Minimal Song"); + expect(result.type).toBeNull(); + expect(result.duration).toBeNull(); + }); + }); + + describe("getById", () => { + test("should return song when exists", async () => { + const created = await repository.create({ name: "Find Me Song" }); + const result = await repository.getById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("Find Me Song"); + }); + + test("should return null when song does not exist", async () => { + const result = await repository.getById(999999); + expect(result).toBeNull(); + }); + }); + + describe("list", () => { + test("should return all songs when no query provided", async () => { + await repository.create({ name: "Song 1" }); + await repository.create({ name: "Song 2" }); + await repository.create({ name: "Song 3" }); + + const result = await repository.list(); + + expect(result.songs.length).toBe(3); + }); + + test("should filter by type", async () => { + await repository.create({ name: "Original Song", type: "ORIGINAL" }); + await repository.create({ name: "Cover Song", type: "COVER" }); + + const result = await repository.list({ type: "ORIGINAL" }); + + expect(result.songs.length).toBe(1); + expect(result.songs[0].type).toBe("ORIGINAL"); + }); + + test("should filter by search term in name", async () => { + await repository.create({ name: "Searchable Song" }); + await repository.create({ name: "Another Song" }); + + const result = await repository.list({ search: "Search" }); + + expect(result.songs.length).toBe(1); + expect(result.songs[0].name).toBe("Searchable Song"); + }); + + test("should filter by search term in description", async () => { + await repository.create({ name: "Song A", description: "Unique description here" }); + await repository.create({ name: "Song B", description: "Different content" }); + + const result = await repository.list({ search: "Unique" }); + + expect(result.songs.length).toBe(1); + expect(result.songs[0].name).toBe("Song A"); + }); + + test("should apply pagination with offset and limit", async () => { + for (let i = 1; i <= 10; i++) { + await repository.create({ name: `Song ${i}` }); + } + + const result = await repository.list({ offset: 2, limit: 3 }); + + expect(result.songs.length).toBe(3); + }); + + test("should exclude soft-deleted songs", async () => { + const song = await repository.create({ name: "To Delete" }); + await repository.softDelete(song.id); + + const result = await repository.list(); + + expect(result.songs.length).toBe(0); + }); + }); + + describe("update", () => { + test("should update all fields", async () => { + const created = await repository.create({ name: "Old Name", type: "ORIGINAL" }); + + const input: UpdateSongDto = { + name: "New Name", + type: "COVER", + duration: 200, + }; + + const result = await repository.update(created.id, input); + + expect(result.name).toBe("New Name"); + expect(result.type).toBe("COVER"); + expect(result.duration).toBe(200); + }); + + test("should update only specified fields", async () => { + const created = await repository.create({ + name: "Original Name", + type: "ORIGINAL", + duration: 100, + }); + + const result = await repository.update(created.id, { name: "Updated Name" }); + + expect(result.name).toBe("Updated Name"); + expect(result.type).toBe("ORIGINAL"); + expect(result.duration).toBe(100); + }); + }); + + describe("softDelete", () => { + test("should soft delete a song", async () => { + const created = await repository.create({ name: "Song to Delete" }); + + await repository.softDelete(created.id); + + const result = await repository.getById(created.id); + expect(result).toBeNull(); + + const allSongs = await repository.list(); + expect(allSongs.songs.length).toBe(0); + }); + }); +}); diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts new file mode 100644 index 0000000..1550500 --- /dev/null +++ b/packages/core/tests/unit/SongService.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test, mock } from "bun:test"; +import { SongService, AppError } from "@project-cvsa/core"; +import type { SongRepository, SongDetailsDto } from "@project-cvsa/core"; + +describe("SongService", () => { + const mockGetDetailsById = mock(); + const mockRepository = { + getDetailsById: mockGetDetailsById, + } as unknown as SongRepository; + const songService = new SongService(mockRepository); + + const mockSongDetails: SongDetailsDto = { + id: 1, + type: "ORIGINAL", + name: "Test Song", + duration: 180, + description: "A test song", + coverUrl: "https://example.com/cover.jpg", + publishedAt: new Date("2024-01-01"), + deletedAt: null, + createdAt: new Date(), + updatedAt: new Date(), + originalSongId: null, + localizedNames: {}, + localizedDescriptions: {}, + bilibiliAid: null, + bilibiliBvid: null, + vocadbId: null, + vcpediaId: null, + moegirlId: null, + singers: [], + artists: [], + }; + + describe("getDetails", () => { + test("returns song details when song exists", async () => { + mockGetDetailsById.mockResolvedValue(mockSongDetails); + + const result = await songService.getDetails(1); + + expect(result).toEqual(mockSongDetails); + expect(mockGetDetailsById).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockGetDetailsById.mockResolvedValue(null); + + expect(songService.getDetails(999)).rejects.toThrow(AppError); + expect(songService.getDetails(999)).rejects.toThrow("Song not found"); + expect(songService.getDetails(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); +}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..7f43608 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@project-cvsa/typescript-config/base.json", + "include": ["modules/**/*.ts", "tests/**/*.ts", "common/**/*.ts", "index.ts"] +} diff --git a/packages/db/package.json b/packages/db/package.json index 8c06bcc..373c4c1 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,26 +1,28 @@ { - "name": "@project-cvsa/db", - "type": "module", - "module": "index.ts", - "scripts": { - "gen": "bunx --bun prisma generate", - "deploy": "", - "migrate": "bunx --bun prisma migrate dev --create-only", - "migrate:dev": "bunx --bun prisma migrate dev", - "push": "bunx --bun prisma db push", - "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", - "studio": "bunx --bun prisma studio" - }, - "devDependencies": { - "prisma": "^7.4.1", - "prisma-json-types-generator": "^4.1.1" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@prisma/adapter-pg": "^7.4.1", - "@prisma/client": "^7.4.1", - "@types/pg": "^8.16.0" - } + "name": "@project-cvsa/db", + "type": "module", + "module": "index.ts", + "scripts": { + "gen": "bunx --bun prisma generate", + "deploy": "", + "migrate": "bunx --bun prisma migrate dev --create-only", + "migrate:dev": "bunx --bun prisma migrate dev", + "push": "bunx --bun prisma db push", + "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", + "studio": "bunx --bun prisma studio", + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "prisma": "^7.4.1", + "prisma-json-types-generator": "^4.1.1", + "@project-cvsa/typescript-config": "workspace:*" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.4.1", + "@prisma/client": "^7.4.1", + "@types/pg": "^8.16.0" + } } diff --git a/packages/db/prisma/models/core/artist.prisma b/packages/db/prisma/models/core/artist.prisma index 0debcae..b57bf59 100644 --- a/packages/db/prisma/models/core/artist.prisma +++ b/packages/db/prisma/models/core/artist.prisma @@ -1,18 +1,18 @@ model Artist { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String? /// [LocalizedField] - localizedNames Json? @map("localized_names") + localizedNames Json? @map("localized_names") aliases String[] description String? /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - artistSongRoles ArtistRole[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User? @relation(fields: [userId], references: [id]) - userId String? @unique @map("user_id") - deletedAt DateTime? @map("deleted_at") + localizedDescriptions Json? @map("localized_descriptions") + creations Creation[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + user User? @relation(fields: [userId], references: [id]) + userId String? @unique @map("user_id") + deletedAt DateTime? @map("deleted_at") @@map("artist") @@schema("core") diff --git a/packages/db/prisma/models/core/artistRole.prisma b/packages/db/prisma/models/core/artistRole.prisma index 40dc72b..6aa38eb 100644 --- a/packages/db/prisma/models/core/artistRole.prisma +++ b/packages/db/prisma/models/core/artistRole.prisma @@ -1,15 +1,12 @@ model ArtistRole { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) role String /// [LocalizedField] - localizedRoles Json? @map("localized_roles") - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - artist Artist @relation(fields: [artistId], references: [id]) - artistId Int @map("artist_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + localizedRoles Json? @map("localized_roles") + creations Creation[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("artist_role") @@schema("core") diff --git a/packages/db/prisma/relations/core/singerOfSong.prisma b/packages/db/prisma/models/core/performance.prisma similarity index 92% rename from packages/db/prisma/relations/core/singerOfSong.prisma rename to packages/db/prisma/models/core/performance.prisma index bd42ca5..113672e 100644 --- a/packages/db/prisma/relations/core/singerOfSong.prisma +++ b/packages/db/prisma/models/core/performance.prisma @@ -1,7 +1,7 @@ // Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.). // In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese). // That's why we need to have both SvsEngine and SvsEngineVersion. -model SingerOfSong { +model Performance { id Int @id @default(autoincrement()) song Song @relation(fields: [songId], references: [id]) songId Int @map("song_id") @@ -10,13 +10,13 @@ model SingerOfSong { voicebank Voicebank? @relation(fields: [voicebankId], references: [id]) voicebankId Int? @map("voicebank_id") svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int? @map("svs_engine_id") + svsEngineId Int? @map("svs_engine_id") svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id]) svsEngineVersionId Int? @map("svs_engine_version_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - @@map("singer_of_song") + @@map("performance") @@schema("core") } diff --git a/packages/db/prisma/models/core/singer.prisma b/packages/db/prisma/models/core/singer.prisma index 7cd3152..278c17b 100644 --- a/packages/db/prisma/models/core/singer.prisma +++ b/packages/db/prisma/models/core/singer.prisma @@ -1,19 +1,17 @@ model Singer { - id Int @id @default(autoincrement()) - name String? - avatarUrl String? @map("avatar_url") + id Int @id @default(autoincrement()) + name String? + avatarUrl String? @map("avatar_url") /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? + localizedNames Json? @map("localized_names") + description String? /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - singerSvsEngineVersions SingerSvsEngineVersion[] - singerSvsEngines SingerSvsEngine[] - voicebanks Voicebank[] - singerOfSongs SingerOfSong[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + localizedDescriptions Json? @map("localized_descriptions") + voicebanks Voicebank[] + performances Performance[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("singer") @@schema("core") diff --git a/packages/db/prisma/models/core/song.prisma b/packages/db/prisma/models/core/song.prisma index 48e6e62..2c0b589 100644 --- a/packages/db/prisma/models/core/song.prisma +++ b/packages/db/prisma/models/core/song.prisma @@ -21,13 +21,13 @@ model Song { moegirlId Int? @map("moegirl_id") externalLinks ExternalLink[] coverUrl String? @map("cover_url") - artists ArtistRole[] - singerOfSongs SingerOfSong[] + creations Creation[] + performances Performance[] + songInSeries SongInSeries[] publishedAt DateTime? @map("published_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - songInSeries SongInSeries[] @@map("song") @@schema("core") diff --git a/packages/db/prisma/models/core/svsEngine.prisma b/packages/db/prisma/models/core/svsEngine.prisma index ac999f5..597d7aa 100644 --- a/packages/db/prisma/models/core/svsEngine.prisma +++ b/packages/db/prisma/models/core/svsEngine.prisma @@ -5,11 +5,10 @@ model SvsEngine { /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") svsEngineVersions SvsEngineVersion[] - singerSvsEngines SingerSvsEngine[] - performances SingerOfSong[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") + performances Performance[] @@map("svs_engine") @@schema("core") diff --git a/packages/db/prisma/models/core/svsEngineVersion.prisma b/packages/db/prisma/models/core/svsEngineVersion.prisma index 154acd7..c742aef 100644 --- a/packages/db/prisma/models/core/svsEngineVersion.prisma +++ b/packages/db/prisma/models/core/svsEngineVersion.prisma @@ -1,14 +1,13 @@ model SvsEngineVersion { - id Int @id @default(autoincrement()) - versionString String @map("version_string") - singerSvsEngineVersions SingerSvsEngineVersion[] - svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int @map("svs_engine_id") - singerOfSongs SingerOfSong[] - voicebanks Voicebank[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + versionString String @map("version_string") + svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) + svsEngineId Int @map("svs_engine_id") + voicebanks Voicebank[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + performances Performance[] @@map("svs_engine_version") @@schema("core") diff --git a/packages/db/prisma/models/core/voicebank.prisma b/packages/db/prisma/models/core/voicebank.prisma index a7adae2..292e062 100644 --- a/packages/db/prisma/models/core/voicebank.prisma +++ b/packages/db/prisma/models/core/voicebank.prisma @@ -8,7 +8,7 @@ model Voicebank { language String svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) svsEngineVersionId Int @map("svs_engine_version_id") - singerOfSongs SingerOfSong[] + performances Performance[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") diff --git a/packages/db/prisma/relations/core/creation.prisma b/packages/db/prisma/relations/core/creation.prisma new file mode 100644 index 0000000..f73f9b3 --- /dev/null +++ b/packages/db/prisma/relations/core/creation.prisma @@ -0,0 +1,16 @@ +model Creation { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + artist Artist @relation(fields: [artistId], references: [id]) + artistId Int @map("artist_id") + role ArtistRole @relation(fields: [artistRoleId], references: [id]) + roleId Int @map("role_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + artistRoleId Int + + @@map("creation") + @@schema("core") +} diff --git a/packages/db/prisma/relations/core/singerSvsEngine.prisma b/packages/db/prisma/relations/core/singerSvsEngine.prisma deleted file mode 100644 index f4440b0..0000000 --- a/packages/db/prisma/relations/core/singerSvsEngine.prisma +++ /dev/null @@ -1,13 +0,0 @@ -model SingerSvsEngine { - id Int @id @default(autoincrement()) - singer Singer @relation(fields: [singerId], references: [id]) - singerId Int @map("singer_id") - svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int @map("svs_engine_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("singer_svs_engine") - @@schema("core") -} diff --git a/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma b/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma deleted file mode 100644 index bc21e73..0000000 --- a/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma +++ /dev/null @@ -1,13 +0,0 @@ -model SingerSvsEngineVersion { - id Int @id @default(autoincrement()) - singer Singer @relation(fields: [singerId], references: [id]) - singerId Int @map("singer_id") - svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) - svsEngineVersionId Int @map("svs_engine_version_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("singer_svs_engine_version") - @@schema("core") -} diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..9fe3128 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@project-cvsa/typescript-config/base.json", + "include": ["index.ts"] +} diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json new file mode 100644 index 0000000..2805cbf --- /dev/null +++ b/packages/typescript-config/base.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["bun"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + } +} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000..036a389 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,4 @@ +{ + "name": "@project-cvsa/typescript-config", + "private": true +} diff --git a/turbo.json b/turbo.json index 2f8df2c..4d56424 100644 --- a/turbo.json +++ b/turbo.json @@ -1,15 +1,21 @@ { - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**", ".output/**"] - }, - "dev": { - "cache": false, - "persistent": true - }, - "lint": {}, - "test": {} - } + "remoteCache": { + "teamSlug": "cvsa", + "apiUrl": "https://turborepo.projectcvsa.com" + }, + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".output/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "test": {}, + "test:coverage": {}, + "typecheck": {} + } } From 81b04bfb61e11653ebb56656afa2f2aa4837fac6 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Fri, 20 Mar 2026 01:16:04 +0800 Subject: [PATCH 06/37] feat(auth): implement login/logout endpoints (#20) - Upgrade biome from 2.3.8 to 2.4.8 - Add turbo.json to apps/backend and packages/core - Update root lint script to use turbo run lint - Fix monorepo workspace name from @project-cvsa/backend to cvsa - Add POST /v2/session for user login with email and password - Add DELETE /v2/session for user logout - Use consistent error message for INVALID_CREDENTIALS - Add 429 rate limit response schema to login/signup endpoints - Switch || to ?? in `betterAuthUserToEntity` for proper nullish coalescing --- .vscode/settings.json | 3 + apps/backend/src/common/middlewares/auth.ts | 22 +--- apps/backend/src/errorHandler.ts | 13 ++ apps/backend/src/modules/auth/index.ts | 8 +- apps/backend/src/modules/auth/login.ts | 71 ++++++++++ apps/backend/src/modules/auth/logout.ts | 31 +++++ apps/backend/src/modules/auth/signup.ts | 6 +- apps/backend/tests/auth.test.ts | 139 ++++++++++++++++++++ apps/backend/turbo.json | 25 ++++ biome.json | 2 +- bun.lock | 137 ++++++++++--------- package.json | 9 +- packages/core/common/error/AppError.ts | 38 +++--- packages/core/modules/auth/user/dto.ts | 66 ++++++++++ packages/core/package.json | 3 +- packages/core/turbo.json | 27 ++++ packages/db/package.json | 1 - turbo.json | 7 +- 18 files changed, 484 insertions(+), 124 deletions(-) create mode 100644 apps/backend/src/modules/auth/login.ts create mode 100644 apps/backend/src/modules/auth/logout.ts create mode 100644 apps/backend/turbo.json create mode 100644 packages/core/turbo.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 0583de5..624bcae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,9 @@ "[javascript]": { "editor.defaultFormatter": "biomejs.biome" }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, "[prisma]": { "editor.defaultFormatter": "Prisma.prisma" } diff --git a/apps/backend/src/common/middlewares/auth.ts b/apps/backend/src/common/middlewares/auth.ts index 2a658f3..ce0add5 100644 --- a/apps/backend/src/common/middlewares/auth.ts +++ b/apps/backend/src/common/middlewares/auth.ts @@ -1,23 +1,5 @@ -import { AppError } from "@project-cvsa/core"; -import { auth } from "@project-cvsa/core"; import { Elysia } from "elysia"; -import type { User } from "@project-cvsa/db"; - -type BetterAuthUser = Exclude>, null>["user"]; - -const convertUser = (user: BetterAuthUser): User => { - const { image, roleId } = user; - return { - ...user, - image: image || null, - roleId: roleId || null, - username: user.username || "", - displayUsername: user.displayUsername || "", - }; -}; - -const toBetterAuthHeaders = (headers: Record): [string, string][] => - Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== undefined); +import { AppError, auth, betterAuthUserToEntity, toBetterAuthHeaders } from "@project-cvsa/core"; export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( { as: "scoped" }, @@ -32,7 +14,7 @@ export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( return { session, - user: convertUser(session.user) as User, + user: betterAuthUserToEntity(session.user), }; } ); diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index 9e05044..8eaf2f5 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -48,6 +48,19 @@ export const errorHandler: ErrorHandler<{ message: error.body?.message, }); } + if ( + [ + "INVALID_CREDENTIALS", + "INVALID_EMAIL_OR_PASSWORD", + "USER_NOT_FOUND", + "EMAIL_NOT_VERIFIED", + ].includes(error.body?.code || "") + ) { + return getErrorResponse(status, 401, { + code: "INVALID_CREDENTIALS", + message: "Provided credentials are invalid.", + }); + } return getErrorResponse(status, error.statusCode, { code: error.body?.code || "AUTH_ERROR", message: error.body?.message, diff --git a/apps/backend/src/modules/auth/index.ts b/apps/backend/src/modules/auth/index.ts index 77a1ee7..7673ad3 100644 --- a/apps/backend/src/modules/auth/index.ts +++ b/apps/backend/src/modules/auth/index.ts @@ -1,5 +1,11 @@ import Elysia from "elysia"; import { signupHandler } from "./signup"; import { getCurrentUserHandler } from "./currentUser"; +import { loginHandler } from "./login"; +import { logoutHandler } from "./logout"; -export const authHandler = new Elysia().use(signupHandler).use(getCurrentUserHandler); +export const authHandler = new Elysia() + .use(signupHandler) + .use(getCurrentUserHandler) + .use(loginHandler) + .use(logoutHandler); diff --git a/apps/backend/src/modules/auth/login.ts b/apps/backend/src/modules/auth/login.ts new file mode 100644 index 0000000..9c2f85b --- /dev/null +++ b/apps/backend/src/modules/auth/login.ts @@ -0,0 +1,71 @@ +import { Elysia } from "elysia"; +import { ip } from "elysia-ip"; +import { rateLimit } from "elysia-rate-limit"; +import { RateLimitError } from "@common/error"; +import { + AppError, + auth, + LoginRequestSchema, + LoginResponseSchema, + ErrorResponseSchema, + betterAuthToLoginUserInfoDto, + toLoginResponse, + toBetterAuthHeaders, +} from "@project-cvsa/core"; + +const DAY = 86400; + +export const loginHandler = new Elysia() + .use(ip()) + .use( + rateLimit({ + scoping: "global", + max: 50, + duration: 5 * 60 * 1000, + generator: () => "", + errorResponse: new RateLimitError(), + }) + ) + .post( + "/session", + async ({ body, status, headers, cookie: { token: tokenCookie } }) => { + const { user, token } = await auth.api.signInEmail({ + body: { + email: body.email, + password: body.password, + }, + headers: toBetterAuthHeaders(headers), + }); + + if (!token) { + throw new AppError("Cannot login", "INTERNAL_SERVER_ERROR", 500, { + cause: "Better Auth responded with no token", + }); + } + + tokenCookie.value = token; + tokenCookie.httpOnly = true; + tokenCookie.maxAge = 90 * DAY; + tokenCookie.secure = true; + tokenCookie.sameSite = "lax"; + + const userInfo = betterAuthToLoginUserInfoDto(user, token); + const response = toLoginResponse(userInfo); + + return status(200, response); + }, + { + body: LoginRequestSchema, + detail: { + summary: "User Login", + description: "", + }, + response: { + 200: LoginResponseSchema, + 401: ErrorResponseSchema, + 422: ErrorResponseSchema, + 429: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/modules/auth/logout.ts b/apps/backend/src/modules/auth/logout.ts new file mode 100644 index 0000000..e9048a9 --- /dev/null +++ b/apps/backend/src/modules/auth/logout.ts @@ -0,0 +1,31 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { authMiddleware } from "@common/middlewares"; +import { auth, ErrorResponseSchema, toBetterAuthHeaders } from "@project-cvsa/core"; + +export const logoutHandler = new Elysia().use(authMiddleware).delete( + "/session", + async ({ set, headers, cookie: { token: tokenCookie } }) => { + await auth.api.signOut({ + headers: toBetterAuthHeaders(headers), + }); + + tokenCookie.remove(); + + set.status = 204; + return null; + }, + { + detail: { + summary: "User Logout", + description: "", + }, + response: { + 204: z.null(), + 401: ErrorResponseSchema, + }, + cookie: z.object({ + token: z.optional(z.string()), + }), + } +); diff --git a/apps/backend/src/modules/auth/signup.ts b/apps/backend/src/modules/auth/signup.ts index c088952..d3ee3f4 100644 --- a/apps/backend/src/modules/auth/signup.ts +++ b/apps/backend/src/modules/auth/signup.ts @@ -8,6 +8,7 @@ import { SignupResponseSchema, toSignUpResponse, ErrorResponseSchema, + toBetterAuthHeaders } from "@project-cvsa/core"; import { RateLimitError } from "@common/error"; import { AppError } from "@project-cvsa/core"; @@ -31,9 +32,7 @@ export const signupHandler = new Elysia() async ({ body, status, headers, cookie: { token: tokenCookie } }) => { const { user, token } = await auth.api.signUpEmail({ body: signupRequestToBetterAuth(body), - headers: Object.entries(headers).filter( - (entry): entry is [string, string] => entry[1] !== undefined - ), + headers: toBetterAuthHeaders(headers), }); if (!token) { @@ -63,6 +62,7 @@ export const signupHandler = new Elysia() 200: SignupResponseSchema, 400: ErrorResponseSchema, 422: ErrorResponseSchema, + 429: ErrorResponseSchema, 500: ErrorResponseSchema, }, } diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index 91721b8..ca0c693 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -182,3 +182,142 @@ describe("Profile E2E Tests - GET /v2/me", () => { expect(status).toBe(401); }); }); + +describe("Login E2E Tests - POST /v2/session", () => { + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.$disconnect(); + }); + + test("should login with valid credentials", async () => { + const signupPayload = { + username: "login_user", + password: "password123", + email: "login@example.com", + }; + + await api.v2.user.post(signupPayload); + + const { data, status } = await api.v2.session.post({ + email: signupPayload.email, + password: signupPayload.password, + }); + + expect(status).toBe(200); + expect(data).toMatchObject({ + message: "Successfully logged in", + data: { + id: expect.any(String), + username: signupPayload.username, + email: signupPayload.email, + token: expect.any(String), + }, + }); + }); + + test("should return 401 on invalid email", async () => { + const signupPayload = { + username: "user_invalid_email", + password: "password123", + email: "invalid@example.com", + }; + + await api.v2.user.post(signupPayload); + + const { error, status } = await api.v2.session.post({ + email: "nonexistent@example.com", + password: signupPayload.password, + }); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "INVALID_CREDENTIALS", + }); + }); + + test("should return 401 on invalid password", async () => { + const signupPayload = { + username: "user_invalid_pass", + password: "password123", + email: "wrongpass@example.com", + }; + + await api.v2.user.post(signupPayload); + + const { error, status } = await api.v2.session.post({ + email: signupPayload.email, + password: "wrongpassword", + }); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "INVALID_CREDENTIALS", + }); + }); + + test("should return 422 on missing fields", async () => { + // @ts-expect-error it's intended + const { error, status } = await api.v2.session.post({}); + + expect(status).toBe(422); + expect(error?.value).toMatchObject({ + code: "VALIDATION_ERROR", + }); + }); +}); + +describe("Logout E2E Tests - DELETE /v2/session", () => { + beforeEach(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.$disconnect(); + }); + + test("should logout with valid token", async () => { + const signupPayload = { + username: "logout_user", + password: "password123", + email: "logout@example.com", + }; + + const { data: signUpData } = await api.v2.user.post(signupPayload); + + const { status } = await api.v2.session.delete( + null, + { + headers: { + authorization: `Bearer ${signUpData?.data.token}`, + }, + } + ); + + expect(status).toBe(204); + }); + + test("should return 401 on invalid/expired token", async () => { + const { error, status } = await api.v2.session.delete( + null, + { + headers: { + authorization: "Bearer invalid.token.format", + }, + } + ); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); +}); diff --git a/apps/backend/turbo.json b/apps/backend/turbo.json new file mode 100644 index 0000000..a576182 --- /dev/null +++ b/apps/backend/turbo.json @@ -0,0 +1,25 @@ +{ + "extends": ["//"], + "tasks": { + "lint": { + "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] + }, + "test": { + "dependsOn": ["typecheck"], + "inputs": [ + "tests/**.ts", + "src/**.ts", + "$TURBO_ROOT$/packages/db/**/*.ts", + "$TURBO_ROOT$/packages/db/**/*.prisma" + ] + }, + "typecheck": { + "inputs": [ + "tests/**.ts", + "src/**.ts", + "tsconfig.json", + "$TURBO_ROOT$/packages/typescript-config/base.json" + ] + } + } +} diff --git a/biome.json b/biome.json index c9b04b4..725c577 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", "assist": { "actions": { "source": { diff --git a/bun.lock b/bun.lock index 4861b90..4acc30a 100644 --- a/bun.lock +++ b/bun.lock @@ -3,10 +3,11 @@ "configVersion": 1, "workspaces": { "": { - "name": "@project-cvsa/backend", + "name": "cvsa", "devDependencies": { - "@biomejs/biome": "2.3.8", - "turbo": "^2.8.16", + "@biomejs/biome": "^2.4.8", + "@types/bun": "^1.3.10", + "turbo": "^2.8.18", }, }, "apps/backend": { @@ -30,7 +31,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", - "@project-cvsa/typescript-config": "*", + "@project-cvsa/typescript-config": "workspace:*", "@types/bun": "latest", }, "peerDependencies": { @@ -49,7 +50,7 @@ }, "devDependencies": { "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/typescript-config": "*", + "@project-cvsa/typescript-config": "workspace:*", "@types/bun": "latest", }, "peerDependencies": { @@ -64,7 +65,7 @@ "@types/pg": "^8.16.0", }, "devDependencies": { - "@project-cvsa/typescript-config": "*", + "@project-cvsa/typescript-config": "workspace:*", "prisma": "^7.4.1", "prisma-json-types-generator": "^4.1.1", }, @@ -97,25 +98,25 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], - "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], @@ -147,37 +148,37 @@ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.5.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.5.0", "@types/pg": "8.11.11", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A=="], - "@prisma/client": ["@prisma/client@7.4.2", "", { "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA=="], + "@prisma/client": ["@prisma/client@7.5.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.5.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w=="], - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.4.2", "", {}, "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.5.0", "", {}, "sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ=="], - "@prisma/config": ["@prisma/config@7.4.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg=="], + "@prisma/config": ["@prisma/config@7.5.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ=="], - "@prisma/debug": ["@prisma/debug@7.4.2", "", {}, "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w=="], + "@prisma/debug": ["@prisma/debug@7.5.0", "", {}, "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg=="], "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], - "@prisma/dmmf": ["@prisma/dmmf@7.4.2", "", {}, "sha512-EurUPjBwmAd69MM8PLZ5n94xeeQ+7SB3Wd9p2+YsgXhvqCa1cgJRBJiB6fTWkPTz5Lf05ySNJ/59EnuEJMAnrw=="], + "@prisma/dmmf": ["@prisma/dmmf@7.5.0", "", {}, "sha512-MAIM8Uji/UuT6PokBwfOHtLHV2GTPpeAwcaFDqNvSZR+rbxrV/gJeLuOjmSnFkGpro/Y0un4oxAkUO7mGQeY6g=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q=="], - "@prisma/engines": ["@prisma/engines@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/fetch-engine": "7.4.2", "@prisma/get-platform": "7.4.2" } }, "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA=="], + "@prisma/engines": ["@prisma/engines@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "@prisma/fetch-engine": "7.5.0", "@prisma/get-platform": "7.5.0" } }, "sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw=="], - "@prisma/engines-version": ["@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "", {}, "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg=="], + "@prisma/engines-version": ["@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "", {}, "sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/get-platform": "7.4.2" } }, "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "@prisma/get-platform": "7.5.0" } }, "sha512-kZCl2FV54qnyrVdnII8MI6qvt7HfU6Cbiz8dZ8PXz4f4lbSw45jEB9/gEMK2SGdiNhBKyk/Wv95uthoLhGMLYA=="], - "@prisma/generator": ["@prisma/generator@7.4.2", "", {}, "sha512-+4B8Rrq5Ef2cm7cW7Wyu6oXn/QVcDVdNti9mKEkcbPVLlA85I6Rbd+j6r+k2E2nlj0zKZbDbq8fIx/Xnt1SmkA=="], + "@prisma/generator": ["@prisma/generator@7.5.0", "", {}, "sha512-GwXeQQ3PFN6EPLCXN8lNjQfC1ssE7wm8AMm7kR8Xbi00REiBn7rB4G5QieW1Nv7zl9rqrpAfG5gfOUU0GAaA+w=="], - "@prisma/generator-helper": ["@prisma/generator-helper@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/dmmf": "7.4.2", "@prisma/generator": "7.4.2" } }, "sha512-PWTkqlMILGVghoWP8SZB3prw/EcthiyPYJsMoiVZrClqPMQn6doPjphCQffPDyGzZvAwmA64edbc6Gm/6Omkjg=="], + "@prisma/generator-helper": ["@prisma/generator-helper@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/dmmf": "7.5.0", "@prisma/generator": "7.5.0" } }, "sha512-qxV/cqoGyKpt9sKlH5k2FoMlpo7gr/dnb2QvCfOaeWYEFanKQLjRE4lr5MovCq3ihfW5oeOpwuiSqz5SG5nlJg=="], "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], - "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], + "@prisma/studio-core": ["@prisma/studio-core@0.21.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-bOGqG/eMQtKC0XVvcVLRmhWWzm/I+0QUWqAEhEBtetpuS3k3V4IWqKGUONkAIT223DNXJMxMtZp36b1FmcdPeg=="], "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], @@ -197,9 +198,21 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.8.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-3QNJ8VR2y1+AZX1HRB2uAvBWHWYpyWMYuDPAPHwhmpC74YweQmPh41dQZ9fkNcgT1eZNgYKf6Q3Hq7i0XK3Ybg=="], + + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kiFEHQ+l64yzRtFcBSdHG9s/EF3aEzzmkuR8e1ygv7ol1YnWwz5EQEbZz6Ajt1sNYYRlo0GXw/d1YanVS9KWPA=="], + + "@turbo/linux-64": ["@turbo/linux-64@2.8.18", "", { "os": "linux", "cpu": "x64" }, "sha512-GlMHBGfob6pOQv7t6imx11O0WJapVVNcJxw+HkliIr1ez8vr8SskM7njqOtqKBTaooCmGA71BFInNRZNYO9qlg=="], + + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-ogWEZ8K/NznjnJU1idHYOA62Y7xYyMnt+kKTZzRNJ9Ye2japtjDkixC31yHaA9QJayb+yS5V8gNDjVfh2yGrIw=="], + + "@turbo/windows-64": ["@turbo/windows-64@2.8.18", "", { "os": "win32", "cpu": "x64" }, "sha512-29g6qryc18ls7BKUmVsjjHAbx2eDgab5CUSPhbCKZYCBfzaW+i79wCzyVoC0NJ8IRNwbLch2QEzgGEoRC6uGOQ=="], + + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-hLsgNDzDDO+Tq2YJ9uvmB2PFTYCgeNj6cyAOOBZP9rsOx0kxNBo4/OvDIgYl/cfo7wsoRBxNQ9giYR+rZMcAjA=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], @@ -255,15 +268,13 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], - "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], - "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], + "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], - "elysia-rate-limit": ["elysia-rate-limit@4.5.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-nsbl3WLvrGiG/SdTgevAsjCUJhY34Bgf+7bDOYrjTPZyS7Hd4MLuLc4MUr9TFsSJWPvKpzyrX2HW8IWjRbex1Q=="], + "elysia-rate-limit": ["elysia-rate-limit@4.5.1", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-36u0BBMWV8Ae5f4q8k19Yuq4UusjbFTstZ7Ej1RU6m/Xc94RZHfai8kn5lOsC+LEduZJr4JdQlZu+jhuSxQfyw=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -275,7 +286,7 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - "file-type": ["file-type@21.3.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA=="], + "file-type": ["file-type@21.3.3", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -307,7 +318,7 @@ "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], - "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + "kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="], "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], @@ -331,9 +342,9 @@ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], - "nanostores": ["nanostores@1.1.1", "", {}, "sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg=="], + "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], "node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], @@ -343,6 +354,8 @@ "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -361,6 +374,8 @@ "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], @@ -381,7 +396,9 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prisma": ["prisma@7.4.2", "", { "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA=="], + "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], + + "prisma": ["prisma@7.5.0", "", { "dependencies": { "@prisma/config": "7.5.0", "@prisma/dev": "0.20.0", "@prisma/engines": "7.5.0", "@prisma/studio-core": "0.21.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-n30qZpWehaYQzigLjmuPisyEsvOzHt7bZeRyg8gZ5DvJo9FGjD+gNaY59Ns3hlLD5/jZH5GBeftIss0jDbUoLg=="], "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], @@ -433,7 +450,7 @@ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], @@ -443,19 +460,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="], - - "turbo-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="], - - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="], - - "turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="], - - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="], - - "turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="], - - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="], + "turbo": ["turbo@2.8.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.18", "@turbo/darwin-arm64": "2.8.18", "@turbo/linux-64": "2.8.18", "@turbo/linux-arm64": "2.8.18", "@turbo/windows-64": "2.8.18", "@turbo/windows-arm64": "2.8.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-ZO5aYR8wXIcZ+KRL8W7RFWbqIRDyYHtesF/g9zPMndmq+KkX34qrZgG9M+YswJ6BGX4jlYC1FlJjleKENagJdw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -477,13 +482,13 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], + "@prisma/adapter-pg/@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="], - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw=="], - "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw=="], - "@project-cvsa/backend/@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -493,22 +498,14 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + "@prisma/adapter-pg/@types/pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], - - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], - - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], - - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], - - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + "@prisma/adapter-pg/@types/pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="], - "@project-cvsa/backend/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@prisma/adapter-pg/@types/pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="], - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@prisma/adapter-pg/@types/pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], } } diff --git a/package.json b/package.json index e891c29..3f54cb5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "dev": "bunx --bun turbo run dev", "build": "bunx --bun turbo run build", - "lint": "biome check .", + "lint": "bunx --bun turbo run lint", "lint:fix": "biome check --write --unsafe .", "format": "biome format --write .", "test": "bunx --bun turbo run test", @@ -16,8 +16,9 @@ "typecheck": "bunx --bun turbo typecheck" }, "devDependencies": { - "@biomejs/biome": "2.3.8", - "turbo": "^2.8.16" + "@biomejs/biome": "^2.4.8", + "@types/bun": "^1.3.10", + "turbo": "^2.8.18" }, - "packageManager": "bun@1.3.10" + "packageManager": "bun@1.3.11" } diff --git a/packages/core/common/error/AppError.ts b/packages/core/common/error/AppError.ts index 1ec5ab1..06131da 100644 --- a/packages/core/common/error/AppError.ts +++ b/packages/core/common/error/AppError.ts @@ -1,28 +1,28 @@ export const APP_ERROR_MAGIC_NUMBER = 0x0712_0412_1099; export class AppError extends Error { - private readonly __type = APP_ERROR_MAGIC_NUMBER; + private readonly __type = APP_ERROR_MAGIC_NUMBER; - constructor( - public override message: string, - public code: string, - public statusCode: number = 500, - public meta?: Record - ) { - super(message); + constructor( + public override message: string, + public code: string, + public statusCode: number = 500, + public meta?: Record + ) { + super(message); this.name = new.target.name; - Error.captureStackTrace(this, new.target); - } + Error.captureStackTrace(this, new.target); + } - static isAppError(obj: unknown): obj is AppError { - if (!obj || typeof obj !== 'object') { - return false; - } + static isAppError(obj: unknown): obj is AppError { + if (!obj || typeof obj !== "object") { + return false; + } - if (obj instanceof AppError) { - return true; - } + if (obj instanceof AppError) { + return true; + } - return '__type' in obj && (obj as { __type: unknown }).__type === APP_ERROR_MAGIC_NUMBER; - } + return "__type" in obj && (obj as { __type: unknown }).__type === APP_ERROR_MAGIC_NUMBER; + } } diff --git a/packages/core/modules/auth/user/dto.ts b/packages/core/modules/auth/user/dto.ts index f4e9a06..2be7d4c 100644 --- a/packages/core/modules/auth/user/dto.ts +++ b/packages/core/modules/auth/user/dto.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import type { auth } from "@project-cvsa/core"; import { getRandomId } from "@project-cvsa/core/common"; +import type { User } from "@project-cvsa/db"; type BetterAuthUser = Exclude>, null>["user"]; @@ -43,6 +44,12 @@ export type SignupUserInfoDto = z.infer; export type CurrentUserInfoDto = z.infer; export type SignupResponseDto = z.infer; +export function toBetterAuthHeaders(headers: Record): [string, string][] { + return Object.entries(headers).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) +} + export function signupRequestToBetterAuth(body: SignupRequestDto): BetterAuthSignupRequestBody { const fakeEmail = `delegate-${getRandomId(14).toLowerCase()}@projectcvsa.com`; @@ -84,3 +91,62 @@ export function toSignUpResponse(data: SignupUserInfoDto): SignupResponseDto { data, }; } + +export const LoginRequestSchema = z.object({ + email: z.email(), + password: z.string().min(1), +}); + +export const LoginUserInfoSchema = z.object({ + id: z.string(), + username: z.string(), + displayName: z.string().optional().nullable(), + email: z.email().optional().nullable(), + token: z.string(), +}); + +export const LoginResponseSchema = z.object({ + message: z.string(), + data: LoginUserInfoSchema, +}); + +export type LoginRequestDto = z.infer; +export type LoginUserInfoDto = z.infer; +export type LoginResponseDto = z.infer; + +export function betterAuthToLoginUserInfoDto( + user: BetterAuthUser, + token: string +): LoginUserInfoDto { + return { + id: user.id, + username: user.username ?? "", + displayName: user.name, + email: user.email, + token, + }; +} + +export function toLoginResponse(data: LoginUserInfoDto): LoginResponseDto { + return { + message: "Successfully logged in", + data, + }; +} + +export const LogoutResponseSchema = z.object({ + message: z.string(), +}); + +export type LogoutResponseDto = z.infer; + +export const betterAuthUserToEntity = (user: BetterAuthUser): User => { + const { image, roleId } = user; + return { + ...user, + image: image ?? null, + roleId: roleId ?? null, + username: user.username ?? "", + displayUsername: user.displayUsername ?? "", + }; +}; diff --git a/packages/core/package.json b/packages/core/package.json index 92fb061..b7cbd19 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,6 +3,7 @@ "type": "module", "module": "index.ts", "scripts": { + "lint": "bunx --bun biome lint", "test": "bun test", "test:coverage": "bun test --coverage", "test:unit": "bun test tests/unit", @@ -12,7 +13,7 @@ "devDependencies": { "@prisma/adapter-pg": "^7.4.1", "@types/bun": "latest", - "@project-cvsa/typescript-config": "*" + "@project-cvsa/typescript-config": "workspace:*" }, "peerDependencies": { "typescript": "^5" diff --git a/packages/core/turbo.json b/packages/core/turbo.json new file mode 100644 index 0000000..543a70e --- /dev/null +++ b/packages/core/turbo.json @@ -0,0 +1,27 @@ +{ + "extends": ["//"], + "tasks": { + "lint": { + "inputs": ["tests/**.ts", "common/**.ts", "modules/**.ts", "biome.json", ".biome*"] + }, + "test": { + "dependsOn": ["typecheck"], + "inputs": [ + "tests/**.ts", + "common/**.ts", + "modules/**.ts", + "$TURBO_ROOT$/packages/db/**/*.ts", + "$TURBO_ROOT$/packages/db/**/*.prisma" + ] + }, + "typecheck": { + "inputs": [ + "tests/**.ts", + "common/**.ts", + "modules/**.ts", + "tsconfig.json", + "$TURBO_ROOT$/packages/typescript-config/base.json" + ] + } + } +} diff --git a/packages/db/package.json b/packages/db/package.json index 373c4c1..ee06f0d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -4,7 +4,6 @@ "module": "index.ts", "scripts": { "gen": "bunx --bun prisma generate", - "deploy": "", "migrate": "bunx --bun prisma migrate dev --create-only", "migrate:dev": "bunx --bun prisma migrate dev", "push": "bunx --bun prisma db push", diff --git a/turbo.json b/turbo.json index 4d56424..f60e003 100644 --- a/turbo.json +++ b/turbo.json @@ -1,9 +1,9 @@ { "remoteCache": { - "teamSlug": "cvsa", - "apiUrl": "https://turborepo.projectcvsa.com" + "enabled": true }, "$schema": "https://turbo.build/schema.json", + "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"], @@ -15,7 +15,6 @@ }, "lint": {}, "test": {}, - "test:coverage": {}, - "typecheck": {} + "test:coverage": {} } } From a44a4a8de8b73a0b0918e13ca68dfca67d006f4b Mon Sep 17 00:00:00 2001 From: paywqiao-max Date: Sun, 22 Mar 2026 00:07:19 +0800 Subject: [PATCH 07/37] refactor(backend): extract songHandler into a separate file (#27) --- apps/backend/src/index.ts | 15 +---- apps/backend/src/modules/auth/currentUser.ts | 56 +++++++++---------- .../backend/src/modules/catalog/song/index.ts | 18 ++++-- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c224eff..ecb7d43 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,14 +2,7 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { - authHandler, - songDetailsHandler, - songCreateHandler, - songUpdateHandler, - songDeleteHandler, - songListHandler, -} from "@modules/index"; +import { authHandler, songHandler } from "@modules/index"; import { AppError } from "@project-cvsa/core"; import { errorHandler } from "./errorHandler"; @@ -27,11 +20,7 @@ export const app = new Elysia({ }) .onError(errorHandler) .use(authHandler) - .use(songDetailsHandler) - .use(songCreateHandler) - .use(songUpdateHandler) - .use(songDeleteHandler) - .use(songListHandler) + .use(songHandler) .use(onAfterHandler) .listen(16412); diff --git a/apps/backend/src/modules/auth/currentUser.ts b/apps/backend/src/modules/auth/currentUser.ts index dbe56f0..6842915 100644 --- a/apps/backend/src/modules/auth/currentUser.ts +++ b/apps/backend/src/modules/auth/currentUser.ts @@ -1,28 +1,28 @@ -import { Elysia } from "elysia"; -import { - CurrentUserInfoSchema, - betterAuthToCurrentUserInfoDto, - ErrorResponseSchema, -} from "@project-cvsa/core"; -import { authMiddleware } from "@common/middlewares"; - -export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( - "/me", - async ({ status, session }) => { - const userInfo = betterAuthToCurrentUserInfoDto(session.user); - return status(200, userInfo); - }, - { - detail: { - summary: "Get current user", - description: "", - }, - response: { - 200: CurrentUserInfoSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 422: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); +import { Elysia } from "elysia"; +import { + CurrentUserInfoSchema, + betterAuthToCurrentUserInfoDto, + ErrorResponseSchema, +} from "@project-cvsa/core"; +import { authMiddleware } from "@common/middlewares"; + +export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( + "/me", + async ({ status, session }) => { + const userInfo = betterAuthToCurrentUserInfoDto(session.user); + return status(200, userInfo); + }, + { + detail: { + summary: "Get current user", + description: "", + }, + response: { + 200: CurrentUserInfoSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 422: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/catalog/song/index.ts b/apps/backend/src/modules/catalog/song/index.ts index d5f6fc3..78c5432 100644 --- a/apps/backend/src/modules/catalog/song/index.ts +++ b/apps/backend/src/modules/catalog/song/index.ts @@ -1,5 +1,13 @@ -export * from "./create"; -export * from "./update"; -export * from "./delete"; -export * from "./list"; -export * from "./details"; +import { Elysia } from "elysia"; +import { songCreateHandler } from "./create"; +import { songUpdateHandler } from "./update"; +import { songDeleteHandler } from "./delete"; +import { songListHandler } from "./list"; +import { songDetailsHandler } from "./details"; + +export const songHandler = new Elysia({ name: "songHandler" }) + .use(songDetailsHandler) + .use(songCreateHandler) + .use(songUpdateHandler) + .use(songDeleteHandler) + .use(songListHandler); From 2fbb50929e9de79140127b4d0cf15f68188203ef Mon Sep 17 00:00:00 2001 From: alikia2x Date: Fri, 27 Mar 2026 18:21:24 +0800 Subject: [PATCH 08/37] chore: add `db:deploy` in scripts, update DATABASE_URL in example env (#28) --- apps/backend/.env.example | 2 +- package.json | 3 ++- packages/core/.env.example | 2 +- packages/db/.env.example | 2 +- packages/db/package.json | 1 + turbo.json | 3 ++- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 701c7b7..ac56e81 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -1 +1 @@ -DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file +DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa diff --git a/package.json b/package.json index 3f54cb5..0576122 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "format": "biome format --write .", "test": "bunx --bun turbo run test", "test:coverage": "bunx --bun turbo run test:coverage", - "typecheck": "bunx --bun turbo typecheck" + "typecheck": "bunx --bun turbo typecheck", + "db:deploy": "bunx --bun turbo db:deploy" }, "devDependencies": { "@biomejs/biome": "^2.4.8", diff --git a/packages/core/.env.example b/packages/core/.env.example index 701c7b7..ac56e81 100644 --- a/packages/core/.env.example +++ b/packages/core/.env.example @@ -1 +1 @@ -DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file +DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa diff --git a/packages/db/.env.example b/packages/db/.env.example index 701c7b7..ac56e81 100644 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1 +1 @@ -DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file +DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa diff --git a/packages/db/package.json b/packages/db/package.json index ee06f0d..bf7221f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -7,6 +7,7 @@ "migrate": "bunx --bun prisma migrate dev --create-only", "migrate:dev": "bunx --bun prisma migrate dev", "push": "bunx --bun prisma db push", + "db:deploy": "bunx --bun prisma migrate deploy", "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", "studio": "bunx --bun prisma studio", "typecheck": "bunx --bun tsc --noEmit" diff --git a/turbo.json b/turbo.json index f60e003..b5a5c23 100644 --- a/turbo.json +++ b/turbo.json @@ -15,6 +15,7 @@ }, "lint": {}, "test": {}, - "test:coverage": {} + "test:coverage": {}, + "db:deploy": {} } } From 5418985ffdaa087a458e0e3ed8cfedcffe6224e9 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sat, 28 Mar 2026 21:03:07 +0800 Subject: [PATCH 09/37] refactor: use more centralized error response schema (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added some custom Biome rules via plugins - introduced a Zod schema generator to Prisma schema - use more specific schema instead of any() or passthough() - Upgrade Prisma dependencies (7.5.0 → 7.6.0) and @cvsa/prisma-zod (3.2.1 → 3.3.0) - Move TypeScript from peerDependencies to root devDependencies - Upgrade to TypeScript 6 - Refactor error handler with factory pattern and explicit error code enum - Simplify SongRepository interface by removing WithRelations variants - Add lyrics field to SongDetailsResponse - Reorganize Prisma schema structure (move performance.prisma to relations/) - Remove relations field (performances, creations) in update method of SongRepository --- AGENTS.md | 4 +- apps/backend/README.md | 108 -- apps/backend/biome.json | 6 + apps/backend/config/no-elysia-typebox.grit | 36 + apps/backend/config/no-try-in-handler.grit | 33 + apps/backend/config/no-zod-any.grit | 9 + apps/backend/package.json | 11 +- .../backend/src/common/error/ConflictError.ts | 2 +- .../src/common/error/RateLimitError.ts | 2 +- .../src/common/error/getErrorResponse.ts | 2 +- apps/backend/src/common/middlewares/auth.ts | 2 +- apps/backend/src/common/prisma.ts | 2 +- apps/backend/src/errorHandler.ts | 140 ++- apps/backend/src/index.ts | 2 +- apps/backend/src/modules/auth/currentUser.ts | 2 +- apps/backend/src/modules/auth/login.ts | 4 +- apps/backend/src/modules/auth/logout.ts | 2 +- apps/backend/src/modules/auth/signup.ts | 8 +- .../src/modules/catalog/song/create.ts | 8 +- .../src/modules/catalog/song/delete.ts | 7 +- .../src/modules/catalog/song/details.ts | 28 +- apps/backend/src/modules/catalog/song/list.ts | 2 +- .../src/modules/catalog/song/update.ts | 10 +- apps/backend/tsconfig.json | 3 +- biome.json | 8 + bun.lock | 187 ++-- package.json | 3 +- packages/core/common/internal.ts | 2 +- packages/core/common/prisma.ts | 4 +- packages/core/common/schema/error.ts | 17 +- packages/core/modules/auth/betterAuth.ts | 4 +- packages/core/modules/auth/user/dto.ts | 6 +- packages/core/modules/catalog/song/dto.ts | 89 +- .../catalog/song/repository.interface.ts | 14 +- .../core/modules/catalog/song/repository.ts | 104 +- packages/core/modules/catalog/song/service.ts | 31 +- packages/core/package.json | 10 +- .../tests/integration/SongRepository.test.ts | 14 +- packages/core/tests/unit/SongService.test.ts | 7 +- packages/core/tsconfig.json | 2 +- packages/db/.gitignore | 1 + packages/db/README.md | 2 +- packages/db/index.ts | 1 + packages/db/package.json | 19 +- .../20260227101459_init/migration.sql | 514 ---------- .../20260310103700_better_auth/migration.sql | 80 -- .../db/prisma/migrations/migration_lock.toml | 3 - packages/db/prisma/models/core/lyrics.prisma | 21 +- .../db/prisma/relations/core/creation.prisma | 3 +- .../core/performance.prisma | 0 packages/db/prisma/schema.prisma | 17 +- packages/db/schema.xml | 955 ++++++++++++++++++ packages/db/tsconfig.json | 2 +- packages/typescript-config/package.json | 2 +- 54 files changed, 1439 insertions(+), 1116 deletions(-) delete mode 100644 apps/backend/README.md create mode 100644 apps/backend/biome.json create mode 100644 apps/backend/config/no-elysia-typebox.grit create mode 100644 apps/backend/config/no-try-in-handler.grit create mode 100644 apps/backend/config/no-zod-any.grit delete mode 100644 packages/db/prisma/migrations/20260227101459_init/migration.sql delete mode 100644 packages/db/prisma/migrations/20260310103700_better_auth/migration.sql delete mode 100644 packages/db/prisma/migrations/migration_lock.toml rename packages/db/prisma/{models => relations}/core/performance.prisma (100%) create mode 100644 packages/db/schema.xml diff --git a/AGENTS.md b/AGENTS.md index 3f0d364..440f618 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ These rules are absolute. Violation will cause pipeline or runtime failures. - **[PROHIBITED]** Inline type assertions: Do not use `as SomeType`, use type annotations. - **[PROHIBITED]** `// @ts-ignore` or `// @ts-expect-error` in first-party code. (Only allowed in third-party code if a GitHub issue URL appended). - **[PROHIBITED]** Defining local interfaces scoped to a single file for API contracts. -- **[REQUIRED]** Import database types directly from `@project-cvsa/db` (e.g., `Song`, `Artist`). +- **[REQUIRED]** Import database types directly from `@cvsa/db` (e.g., `Song`, `Artist`). ### Validation & Zod - **[PROHIBITED]** `z.any()` in any schema. @@ -70,7 +70,7 @@ These rules are absolute. Violation will cause pipeline or runtime failures. ### Import Hierarchy Order imports from farthest to closest. Prioritize path aliases 1. External: Built-in and external NPM packages. -2. Workspace: Internal packages (`@project-cvsa/core`, `@project-cvsa/db`). +2. Workspace: Internal packages (`@cvsa/core`, `@cvsa/db`). 3. Aliases: `@/*`, `@modules/*`, `@common/*` pointing to `./src/`. 4. **[PROHIBITED]**: relative paths (`../`, `./`, `../../../`, etc.) diff --git a/apps/backend/README.md b/apps/backend/README.md deleted file mode 100644 index ab98466..0000000 --- a/apps/backend/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# CVSA Backend - -The backend service for the **Chinese Vocal Synthesizer Archive (CVSA)** – a unified platform for the Chinese vocal synthesizer community. -This service provides a RESTful API for the rest of the CVSA system and third parties to interact with. - -## Tech Stack - -| Layer | Technology | -| ----------- | --------------------------------------------------------------------------------------- | -| Runtime | [Bun](https://bun.sh) – fast JavaScript runtime, package manager, and test runner | -| Framework | [Elysia](https://elysiajs.com) – high‑performance, type‑friendly web framework | -| Database | PostgreSQL | -| ORM | [Prisma](https://www.prisma.io) – type‑safe database client with migrations | -| Testing | Bun test (unit, integration, E2E) | - -## Getting Started - -### Prerequisites - -- [Bun](https://bun.sh) >= 1.3.10 -- PostgreSQL >= 18 - -### Installation - -1. **Clone the repository** - - ```bash - git clone https://github.com/project-cvsa/cvsa-backend - cd cvsa-backend - ``` - -2. **Install dependencies** - - ```bash - bun install - ``` - -3. **Set up environment variables** - Copy the example file and edit it with your database credentials: - - ```bash - cp .env.example .env - ``` - - Required variables: - - ```ini - DATABASE_URL="postgresql://user:password@localhost:5432/cvsa" - ``` - -4. **Generate Prisma client and run migrations** - - ```bash - bun db-gen # generate Prisma Client - bun migrate:dev # apply migrations to your database - ``` - - (For production, use `bun migrate` with custom scripts.) - -5. **Start the development server** - - ```bash - bun dev - ``` - - The server will start at `http://localhost:16412` (configurable via `PORT`). - -## Testing - -We use Bun’s built‑in test runner. - -Run all tests: - -```bash -bun test -``` - -For a specific suite: - -```bash -bun test:unit -bun test:integration -bun test:e2e -``` - -To set up the test database (requires `DATABASE_URL` pointing to a test database): - -```bash -bun test:db:setup # runs migrations and resets the test DB -``` - - -## Package Structure - -``` -src/ -├── containers.ts # DI container wiring -├── index.ts # App entry point -├── onAfterHandle.ts # Global response handler -├── startMessage.ts # Startup logging -├── lib/ # Shared utilities -├── repositories/ # Data access layer -├── services/ # Business logic -├── routes/ # Elysia route definitions -└── schemas/ # Zod validation schemas -tests/ # Unit, integration, E2E tests -``` - diff --git a/apps/backend/biome.json b/apps/backend/biome.json new file mode 100644 index 0000000..1126570 --- /dev/null +++ b/apps/backend/biome.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", + "root": false, + "extends": "//", + "plugins": ["./config/no-elysia-typebox.grit", "./config/no-try-in-handler.grit", "./config/no-zod-any.grit"] +} diff --git a/apps/backend/config/no-elysia-typebox.grit b/apps/backend/config/no-elysia-typebox.grit new file mode 100644 index 0000000..85b00c4 --- /dev/null +++ b/apps/backend/config/no-elysia-typebox.grit @@ -0,0 +1,36 @@ +engine biome(1.0) +language js(typescript) + +pattern from_module($name) { + JsModuleSource() as $m where { + $m <: `$name` + } +} + +pattern disallowed_identifier($name) { + JsIdentifierBinding() as $ib where { + $ib <: `$name`, + register_diagnostic( + span=$ib, + message="Avoid using Elysia TypeBox. Use Zod schema instead." + ) + } +} + +pattern disallowed_literal_export($name) { + JsLiteralExportName() as $en where { + $en <: $name, + register_diagnostic( + span=$en, + message="Avoid using Elysia TypeBox. Use Zod schema instead." + ) + } +} + +JsImport() as $i where { + $i <: or { + contains disallowed_identifier(name = `t`), + contains disallowed_literal_export(name = "t") + }, + $i <: contains from_module(name = `"elysia"`) +} diff --git a/apps/backend/config/no-try-in-handler.grit b/apps/backend/config/no-try-in-handler.grit new file mode 100644 index 0000000..a313a62 --- /dev/null +++ b/apps/backend/config/no-try-in-handler.grit @@ -0,0 +1,33 @@ +engine biome(1.0) +language js(typescript) + +pattern new_expr($identifier) { + JsNewExpression() as $n where { + $n <: contains JsIdentifierExpression() as $ie where { + $ie <: contains JsReferenceIdentifier() as $i where { + $i <: `$identifier` + } + } + } +} + +JsCallExpression() as $c where { + $c <: contains JsStaticMemberExpression() as $sme where { + $sme <: contains JsName() as $jsn where { + $jsn <: or { `get`, `post`, `put`, `patch`, `delete`, `all` } + } + }, + $c <: contains new_expr(identifier = `Elysia`), + $c <: contains JsCallArgumentList[$arg1, $arg2, ...] where { + $arg1 <: contains JsStringLiteralExpression(), + $arg2 <: contains JsArrowFunctionExpression() as $func where { + $func <: contains JsTryStatement() as $try where { + register_diagnostic( + span=$try, + message="Avoid using try-catch statement in route handlers. Errors should be handled by separate ErrorHandler.", + severity="warn" + ) + } + } + } +} diff --git a/apps/backend/config/no-zod-any.grit b/apps/backend/config/no-zod-any.grit new file mode 100644 index 0000000..b2774fa --- /dev/null +++ b/apps/backend/config/no-zod-any.grit @@ -0,0 +1,9 @@ +engine biome(1.0) +language js(typescript) + +`z.any()` as $any where { + register_diagnostic( + span = $any, + message = "Unexpected z.any(). Specify a different schema." + ) +} diff --git a/apps/backend/package.json b/apps/backend/package.json index c4a556b..3d6c806 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,5 +1,5 @@ { - "name": "@project-cvsa/backend", + "name": "@cvsa/backend", "type": "module", "version": "0.0.1", "scripts": { @@ -13,18 +13,15 @@ "devDependencies": { "@biomejs/biome": "^2.4.4", "@types/bun": "latest", - "@project-cvsa/typescript-config": "workspace:*" - }, - "peerDependencies": { - "typescript": "^5" + "@cvsa/typescript-config": "workspace:*" }, "dependencies": { "@better-auth/prisma-adapter": "^1.5.4", "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/db": "workspace:*", + "@cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", - "@project-cvsa/core": "workspace:*", + "@cvsa/core": "workspace:*", "argon2": "^0.44.0", "better-auth": "^1.5.4", "chalk": "^5.6.2", diff --git a/apps/backend/src/common/error/ConflictError.ts b/apps/backend/src/common/error/ConflictError.ts index 75fbf65..d93345e 100644 --- a/apps/backend/src/common/error/ConflictError.ts +++ b/apps/backend/src/common/error/ConflictError.ts @@ -1,4 +1,4 @@ -import { AppError } from "@project-cvsa/core"; +import { AppError } from "@cvsa/core"; export class ConflictError extends AppError { constructor( diff --git a/apps/backend/src/common/error/RateLimitError.ts b/apps/backend/src/common/error/RateLimitError.ts index 91ec473..c192204 100644 --- a/apps/backend/src/common/error/RateLimitError.ts +++ b/apps/backend/src/common/error/RateLimitError.ts @@ -1,4 +1,4 @@ -import { AppError } from "@project-cvsa/core"; +import { AppError } from "@cvsa/core"; export class RateLimitError extends AppError { constructor(message = "Request rate limit exceeded") { diff --git a/apps/backend/src/common/error/getErrorResponse.ts b/apps/backend/src/common/error/getErrorResponse.ts index efa369b..af86344 100644 --- a/apps/backend/src/common/error/getErrorResponse.ts +++ b/apps/backend/src/common/error/getErrorResponse.ts @@ -1,4 +1,4 @@ -import type { ErrorResponseDto } from "@project-cvsa/core"; +import type { ErrorResponseDto } from "@cvsa/core"; // biome-ignore lint/suspicious/noExplicitAny: Utility type type StatusFunc = (code: number, response: any) => any; diff --git a/apps/backend/src/common/middlewares/auth.ts b/apps/backend/src/common/middlewares/auth.ts index ce0add5..2c7cd51 100644 --- a/apps/backend/src/common/middlewares/auth.ts +++ b/apps/backend/src/common/middlewares/auth.ts @@ -1,5 +1,5 @@ import { Elysia } from "elysia"; -import { AppError, auth, betterAuthUserToEntity, toBetterAuthHeaders } from "@project-cvsa/core"; +import { AppError, auth, betterAuthUserToEntity, toBetterAuthHeaders } from "@cvsa/core"; export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( { as: "scoped" }, diff --git a/apps/backend/src/common/prisma.ts b/apps/backend/src/common/prisma.ts index 562cfe0..cbfe2d5 100644 --- a/apps/backend/src/common/prisma.ts +++ b/apps/backend/src/common/prisma.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from "@project-cvsa/db"; +import { PrismaClient } from "@cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; import { env } from "@common/env"; diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index 8eaf2f5..9ac2c36 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -1,73 +1,103 @@ -import { ZodError } from "zod"; import { APIError } from "better-auth"; import type { ErrorHandler } from "elysia"; -import { AppError } from "@project-cvsa/core"; +import { AppError } from "@cvsa/core"; +import type { ErrorResponseDto } from "@cvsa/core"; import { getErrorResponse } from "@/common/error"; -export const errorHandler: ErrorHandler<{ - readonly AppError: AppError; -}> = ({ code, status, error }) => { - if (AppError.isAppError(error)) { - return getErrorResponse(status, error.statusCode, { - code: error.code, +const AUTH_CONFLICT_CODES = [ + "USERNAME_IS_ALREADY_TAKEN", + "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", +] as const; + +const AUTH_INVALID_CODES = [ + "INVALID_CREDENTIALS", + "INVALID_EMAIL_OR_PASSWORD", + "USER_NOT_FOUND", + "EMAIL_NOT_VERIFIED", +] as const; + +type AuthConflictCode = (typeof AUTH_CONFLICT_CODES)[number]; +type AuthInvalidCode = (typeof AUTH_INVALID_CODES)[number]; + +type StatusDecorator = ReturnType>["status"]; + +const response = { + appError: (status: StatusDecorator, error: AppError) => + getErrorResponse(status, error.statusCode, { + code: error.code as ErrorResponseDto["code"], message: error.message, - }); - } - if (code === "NOT_FOUND") - return getErrorResponse(status, 404, { + }), + + notFound: (status: StatusDecorator) => + getErrorResponse(status, 404, { code: "NOT_FOUND", message: "The requested resource was not found.", - }); - if (code === "VALIDATION") { + }), + + validation: ( + status: StatusDecorator, + error: { message: string; detail: (msg: string) => unknown } + ) => { const detail = error.detail(error.message); - if (typeof detail === "string") { - return getErrorResponse(status, 422, { - code: "VALIDATION_ERROR", - message: detail, - }); - } return getErrorResponse(status, 422, { code: "VALIDATION_ERROR", - message: detail.summary, + message: typeof detail === "string" ? detail : (detail as { summary: string }).summary, }); + }, + + betterAuthConflict: (status: StatusDecorator, error: APIError) => + getErrorResponse(status, 409, { + code: (error.body?.code || "ENTITY_CONFLICT") as ErrorResponseDto["code"], + message: error.body?.message, + }), + + betterAuthInvalidCred: (status: StatusDecorator) => + getErrorResponse(status, 401, { + code: "INVALID_CREDENTIALS", + message: "Provided credentials are invalid.", + }), + + betterAuthGeneral: (status: StatusDecorator, error: APIError) => + getErrorResponse(status, error.statusCode, { + code: (error.body?.code || "AUTH_ERROR") as ErrorResponseDto["code"], + message: error.body?.message, + }), + + internal: (status: StatusDecorator) => + getErrorResponse(status, 500, { + code: "INTERNAL_SERVER_ERROR", + message: "Internal server error", + }), +} as const; + +export const errorHandler: ErrorHandler<{ + readonly AppError: AppError; +}> = ({ code, status, error }) => { + if (AppError.isAppError(error)) { + return response.appError(status, error); } - if (error instanceof ZodError) { - return getErrorResponse(status, 422, { - code: "VALIDATION_ERROR", - message: error.message, - }); + + if (code === "NOT_FOUND") { + return response.notFound(status); } + + if (code === "VALIDATION") { + return response.validation(status, error); + } + if (error instanceof APIError) { - if ( - ["USERNAME_IS_ALREADY_TAKEN", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"].includes( - error.body?.code || "" - ) - ) { - return getErrorResponse(status, 409, { - code: error.body?.code || "ENTITY_CONFLICT", - message: error.body?.message, - }); + const bodyCode = error.body?.code || ""; + + if (AUTH_CONFLICT_CODES.includes(bodyCode as AuthConflictCode)) { + return response.betterAuthConflict(status, error); } - if ( - [ - "INVALID_CREDENTIALS", - "INVALID_EMAIL_OR_PASSWORD", - "USER_NOT_FOUND", - "EMAIL_NOT_VERIFIED", - ].includes(error.body?.code || "") - ) { - return getErrorResponse(status, 401, { - code: "INVALID_CREDENTIALS", - message: "Provided credentials are invalid.", - }); + + if (AUTH_INVALID_CODES.includes(bodyCode as AuthInvalidCode)) { + return response.betterAuthInvalidCred(status); } - return getErrorResponse(status, error.statusCode, { - code: error.body?.code || "AUTH_ERROR", - message: error.body?.message, - }); + + return response.betterAuthGeneral(status, error); } - return getErrorResponse(status, 500, { - code: "SERVER_ERROR", - message: "Internal server error", - }); + + return response.internal(status); }; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index ecb7d43..1e35d73 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -3,7 +3,7 @@ import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; import { authHandler, songHandler } from "@modules/index"; -import { AppError } from "@project-cvsa/core"; +import { AppError } from "@cvsa/core"; import { errorHandler } from "./errorHandler"; const [host, port] = getBindingInfo(); diff --git a/apps/backend/src/modules/auth/currentUser.ts b/apps/backend/src/modules/auth/currentUser.ts index 6842915..a549a18 100644 --- a/apps/backend/src/modules/auth/currentUser.ts +++ b/apps/backend/src/modules/auth/currentUser.ts @@ -3,7 +3,7 @@ import { CurrentUserInfoSchema, betterAuthToCurrentUserInfoDto, ErrorResponseSchema, -} from "@project-cvsa/core"; +} from "@cvsa/core"; import { authMiddleware } from "@common/middlewares"; export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( diff --git a/apps/backend/src/modules/auth/login.ts b/apps/backend/src/modules/auth/login.ts index 9c2f85b..ddb6e63 100644 --- a/apps/backend/src/modules/auth/login.ts +++ b/apps/backend/src/modules/auth/login.ts @@ -11,7 +11,7 @@ import { betterAuthToLoginUserInfoDto, toLoginResponse, toBetterAuthHeaders, -} from "@project-cvsa/core"; +} from "@cvsa/core"; const DAY = 86400; @@ -19,7 +19,7 @@ export const loginHandler = new Elysia() .use(ip()) .use( rateLimit({ - scoping: "global", + scoping: "scoped", max: 50, duration: 5 * 60 * 1000, generator: () => "", diff --git a/apps/backend/src/modules/auth/logout.ts b/apps/backend/src/modules/auth/logout.ts index e9048a9..35e4399 100644 --- a/apps/backend/src/modules/auth/logout.ts +++ b/apps/backend/src/modules/auth/logout.ts @@ -1,7 +1,7 @@ import { Elysia } from "elysia"; import { z } from "zod"; import { authMiddleware } from "@common/middlewares"; -import { auth, ErrorResponseSchema, toBetterAuthHeaders } from "@project-cvsa/core"; +import { auth, ErrorResponseSchema, toBetterAuthHeaders } from "@cvsa/core"; export const logoutHandler = new Elysia().use(authMiddleware).delete( "/session", diff --git a/apps/backend/src/modules/auth/signup.ts b/apps/backend/src/modules/auth/signup.ts index d3ee3f4..5c204f0 100644 --- a/apps/backend/src/modules/auth/signup.ts +++ b/apps/backend/src/modules/auth/signup.ts @@ -9,10 +9,10 @@ import { toSignUpResponse, ErrorResponseSchema, toBetterAuthHeaders -} from "@project-cvsa/core"; +} from "@cvsa/core"; import { RateLimitError } from "@common/error"; -import { AppError } from "@project-cvsa/core"; -import { auth } from "@project-cvsa/core"; +import { AppError } from "@cvsa/core"; +import { auth } from "@cvsa/core"; const DAY = 86400; @@ -20,7 +20,7 @@ export const signupHandler = new Elysia() .use(ip()) .use( rateLimit({ - scoping: "global", + scoping: "scoped", max: 50, duration: 5 * 60 * 1000, generator: () => "", // global limit diff --git a/apps/backend/src/modules/catalog/song/create.ts b/apps/backend/src/modules/catalog/song/create.ts index addd974..b93530e 100644 --- a/apps/backend/src/modules/catalog/song/create.ts +++ b/apps/backend/src/modules/catalog/song/create.ts @@ -1,9 +1,7 @@ import { Elysia } from "elysia"; -import { z } from "zod"; -import { SongService, createSongSchema, ErrorResponseSchema } from "@project-cvsa/core"; +import { SongService, CreateSongRequestSchema, ErrorResponseSchema } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; - -const CreateSongRequestSchema = createSongSchema; +import { SongSchema } from "@cvsa/db"; export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( "/song", @@ -18,7 +16,7 @@ export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(a description: "Create a new song (authentication required)", }, response: { - 201: z.object({}).passthrough(), + 201: SongSchema, 401: ErrorResponseSchema, }, } diff --git a/apps/backend/src/modules/catalog/song/delete.ts b/apps/backend/src/modules/catalog/song/delete.ts index 0715977..68d5e8e 100644 --- a/apps/backend/src/modules/catalog/song/delete.ts +++ b/apps/backend/src/modules/catalog/song/delete.ts @@ -1,7 +1,7 @@ import { Elysia } from "elysia"; import { z } from "zod"; -import { SongService, ErrorResponseSchema } from "@project-cvsa/core"; -import { AppError } from "@project-cvsa/core"; +import { SongService, ErrorResponseSchema } from "@cvsa/core"; +import { AppError } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) @@ -15,6 +15,7 @@ export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) } await new SongService().delete(id); set.status = 204; + return null; }, { detail: { @@ -22,7 +23,7 @@ export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) description: "Soft delete a song (authentication required)", }, response: { - 204: z.undefined(), + 204: z.null(), 400: ErrorResponseSchema, 401: ErrorResponseSchema, 404: ErrorResponseSchema, diff --git a/apps/backend/src/modules/catalog/song/details.ts b/apps/backend/src/modules/catalog/song/details.ts index 2a1e06e..d8c9e5c 100644 --- a/apps/backend/src/modules/catalog/song/details.ts +++ b/apps/backend/src/modules/catalog/song/details.ts @@ -1,26 +1,12 @@ import { Elysia } from "elysia"; -import { z } from "zod"; -import { SongService, ErrorResponseSchema, type SongDetailsDto } from "@project-cvsa/core"; -import { AppError } from "@project-cvsa/core"; - -const SongDetailsResponseSchema: z.ZodType = z.any(); +import { SongService, ErrorResponseSchema, SongDetailsResponseSchema } from "@cvsa/core"; +import z from "zod"; export const songDetailsHandler = new Elysia().get( "/song/:id/details", - async ({ params, status }) => { - const id = Number(params.id); - try { - const song = await new SongService().getDetails(id); - return song; - } catch (e) { - if (e instanceof AppError && e.code === "NOT_FOUND") { - return status(404, { - code: e.code, - message: e.message, - }); - } - throw e; - } + async ({ params }) => { + const song = await new SongService().getDetails(params.id); + return song; }, { detail: { @@ -29,7 +15,11 @@ export const songDetailsHandler = new Elysia().get( }, response: { 200: SongDetailsResponseSchema, + 400: ErrorResponseSchema, 404: ErrorResponseSchema, }, + params: z.object({ + id: z.int().positive(), + }), } ); diff --git a/apps/backend/src/modules/catalog/song/list.ts b/apps/backend/src/modules/catalog/song/list.ts index a8af923..84b6d87 100644 --- a/apps/backend/src/modules/catalog/song/list.ts +++ b/apps/backend/src/modules/catalog/song/list.ts @@ -1,5 +1,5 @@ import { Elysia } from "elysia"; -import { SongService, ListSongsQuerySchema, ListSongsResponseSchema } from "@project-cvsa/core"; +import { SongService, ListSongsQuerySchema, ListSongsResponseSchema } from "@cvsa/core"; export const songListHandler = new Elysia({ name: "songListHandler" }).get( "/song", diff --git a/apps/backend/src/modules/catalog/song/update.ts b/apps/backend/src/modules/catalog/song/update.ts index 5bec45d..d76571c 100644 --- a/apps/backend/src/modules/catalog/song/update.ts +++ b/apps/backend/src/modules/catalog/song/update.ts @@ -1,10 +1,8 @@ import { Elysia } from "elysia"; -import { z } from "zod"; -import { SongService, UpdateSongSchema, ErrorResponseSchema } from "@project-cvsa/core"; -import { AppError } from "@project-cvsa/core"; +import { SongService, UpdateSongRequestSchema, ErrorResponseSchema } from "@cvsa/core"; +import { AppError } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; - -const UpdateSongRequestSchema = UpdateSongSchema; +import { SongSchema } from "@cvsa/db"; export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }).use(authMiddleware).put( "/song/:id", @@ -23,7 +21,7 @@ export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }).use(a description: "Update an existing song (authentication required)", }, response: { - 200: z.object({}).passthrough(), + 200: SongSchema, 400: ErrorResponseSchema, 401: ErrorResponseSchema, 404: ErrorResponseSchema, diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 48b006c..369cbec 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "@project-cvsa/typescript-config/base.json", + "extends": "@cvsa/typescript-config/base.json", "compilerOptions": { - "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@modules/*": ["./src/modules/*"], diff --git a/biome.json b/biome.json index 725c577..74ee388 100644 --- a/biome.json +++ b/biome.json @@ -16,6 +16,14 @@ "lineWidth": 100, "formatWithErrors": true }, + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "error", + "noDeprecatedImports": "error" + } + } + }, "javascript": { "formatter": { "enabled": true, diff --git a/bun.lock b/bun.lock index 4acc30a..1422d70 100644 --- a/bun.lock +++ b/bun.lock @@ -8,17 +8,18 @@ "@biomejs/biome": "^2.4.8", "@types/bun": "^1.3.10", "turbo": "^2.8.18", + "typescript": "^6.0.2", }, }, "apps/backend": { - "name": "@project-cvsa/backend", + "name": "@cvsa/backend", "version": "0.0.1", "dependencies": { "@better-auth/prisma-adapter": "^1.5.4", + "@cvsa/core": "workspace:*", + "@cvsa/db": "workspace:*", "@elysiajs/eden": "^1.0.5", "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/core": "workspace:*", - "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", "better-auth": "^1.5.4", @@ -31,50 +32,44 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", - "@project-cvsa/typescript-config": "workspace:*", + "@cvsa/typescript-config": "workspace:*", "@types/bun": "latest", }, - "peerDependencies": { - "typescript": "^5", - }, }, "packages/core": { - "name": "@project-cvsa/core", + "name": "@cvsa/core", "dependencies": { "@better-auth/prisma-adapter": "^1.5.5", - "@project-cvsa/db": "*", + "@cvsa/db": "*", "@t3-oss/env-core": "^0.13.10", "better-auth": "^1.5.5", "nanoid": "^5.1.6", + "remeda": "^2.33.6", "zod": "^4.3.6", }, "devDependencies": { + "@cvsa/typescript-config": "workspace:*", "@prisma/adapter-pg": "^7.4.1", - "@project-cvsa/typescript-config": "workspace:*", "@types/bun": "latest", }, - "peerDependencies": { - "typescript": "^5", - }, }, "packages/db": { - "name": "@project-cvsa/db", + "name": "@cvsa/db", "dependencies": { - "@prisma/adapter-pg": "^7.4.1", - "@prisma/client": "^7.4.1", + "@prisma/adapter-pg": "^7.6.0", + "@prisma/client": "^7.6.0", "@types/pg": "^8.16.0", + "zod": "^4.3.6", }, "devDependencies": { - "@project-cvsa/typescript-config": "workspace:*", - "prisma": "^7.4.1", + "@cvsa/prisma-zod": "^3.3.0", + "@cvsa/typescript-config": "workspace:*", + "prisma": "^7.6.0", "prisma-json-types-generator": "^4.1.1", }, - "peerDependencies": { - "typescript": "^5", - }, }, "packages/typescript-config": { - "name": "@project-cvsa/typescript-config", + "name": "@cvsa/typescript-config", }, }, "packages": { @@ -118,29 +113,35 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], + "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + + "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + + "@cvsa/backend": ["@cvsa/backend@workspace:apps/backend"], + + "@cvsa/core": ["@cvsa/core@workspace:packages/core"], - "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + "@cvsa/db": ["@cvsa/db@workspace:packages/db"], - "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], + "@cvsa/prisma-zod": ["@cvsa/prisma-zod@3.3.0", "", { "dependencies": { "@prisma/generator-helper": "^7.5.0" }, "peerDependencies": { "@prisma/client": ">= 7.0.0", "prisma": ">= 7.0.0", "zod": "^4.3.6" }, "bin": { "prisma-zod": "dist/index.js" } }, "sha512-FaLH1aNBAbS5Vd96G9TCPR/piacRf2zWNL8UcheTHAvcaGNCLOCEOAthpelcmI8YKU2FDXxpmZZSqSz3hj8ZbA=="], - "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + "@cvsa/typescript-config": ["@cvsa/typescript-config@workspace:packages/typescript-config"], - "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="], - "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="], + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="], - "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="], "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], @@ -148,27 +149,27 @@ "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.5.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.5.0", "@types/pg": "8.11.11", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="], - "@prisma/client": ["@prisma/client@7.5.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.5.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w=="], + "@prisma/client": ["@prisma/client@7.6.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.6.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ=="], - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.5.0", "", {}, "sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.6.0", "", {}, "sha512-fD7jlqubsZvVODKvsp9lOpXVecx2aWGxC2l35Ioz2t+teUJ5CfR0SAMsi7UkU1VvaZmmm+DS6BdujF622nY7tQ=="], - "@prisma/config": ["@prisma/config@7.5.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ=="], + "@prisma/config": ["@prisma/config@7.6.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw=="], - "@prisma/debug": ["@prisma/debug@7.5.0", "", {}, "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg=="], + "@prisma/debug": ["@prisma/debug@7.6.0", "", {}, "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA=="], - "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], + "@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="], "@prisma/dmmf": ["@prisma/dmmf@7.5.0", "", {}, "sha512-MAIM8Uji/UuT6PokBwfOHtLHV2GTPpeAwcaFDqNvSZR+rbxrV/gJeLuOjmSnFkGpro/Y0un4oxAkUO7mGQeY6g=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], - "@prisma/engines": ["@prisma/engines@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "@prisma/fetch-engine": "7.5.0", "@prisma/get-platform": "7.5.0" } }, "sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw=="], + "@prisma/engines": ["@prisma/engines@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/fetch-engine": "7.6.0", "@prisma/get-platform": "7.6.0" } }, "sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w=="], - "@prisma/engines-version": ["@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "", {}, "sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA=="], + "@prisma/engines-version": ["@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "", {}, "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", "@prisma/get-platform": "7.5.0" } }, "sha512-kZCl2FV54qnyrVdnII8MI6qvt7HfU6Cbiz8dZ8PXz4f4lbSw45jEB9/gEMK2SGdiNhBKyk/Wv95uthoLhGMLYA=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/get-platform": "7.6.0" } }, "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w=="], "@prisma/generator": ["@prisma/generator@7.5.0", "", {}, "sha512-GwXeQQ3PFN6EPLCXN8lNjQfC1ssE7wm8AMm7kR8Xbi00REiBn7rB4G5QieW1Nv7zl9rqrpAfG5gfOUU0GAaA+w=="], @@ -178,15 +179,25 @@ "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], - "@prisma/studio-core": ["@prisma/studio-core@0.21.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-bOGqG/eMQtKC0XVvcVLRmhWWzm/I+0QUWqAEhEBtetpuS3k3V4IWqKGUONkAIT223DNXJMxMtZp36b1FmcdPeg=="], + "@prisma/streams-local": ["@prisma/streams-local@0.1.2", "", { "dependencies": { "ajv": "^8.12.0", "better-result": "^2.7.0", "env-paths": "^3.0.0", "proper-lockfile": "^4.1.2" } }, "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg=="], + + "@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@project-cvsa/core": ["@project-cvsa/core@workspace:packages/core"], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@project-cvsa/db": ["@project-cvsa/db@workspace:packages/db"], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@project-cvsa/typescript-config": ["@project-cvsa/typescript-config@workspace:packages/typescript-config"], + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], @@ -198,23 +209,23 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.8.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-3QNJ8VR2y1+AZX1HRB2uAvBWHWYpyWMYuDPAPHwhmpC74YweQmPh41dQZ9fkNcgT1eZNgYKf6Q3Hq7i0XK3Ybg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kiFEHQ+l64yzRtFcBSdHG9s/EF3aEzzmkuR8e1ygv7ol1YnWwz5EQEbZz6Ajt1sNYYRlo0GXw/d1YanVS9KWPA=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.8.18", "", { "os": "linux", "cpu": "x64" }, "sha512-GlMHBGfob6pOQv7t6imx11O0WJapVVNcJxw+HkliIr1ez8vr8SskM7njqOtqKBTaooCmGA71BFInNRZNYO9qlg=="], + "@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-ogWEZ8K/NznjnJU1idHYOA62Y7xYyMnt+kKTZzRNJ9Ye2japtjDkixC31yHaA9QJayb+yS5V8gNDjVfh2yGrIw=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="], - "@turbo/windows-64": ["@turbo/windows-64@2.8.18", "", { "os": "win32", "cpu": "x64" }, "sha512-29g6qryc18ls7BKUmVsjjHAbx2eDgab5CUSPhbCKZYCBfzaW+i79wCzyVoC0NJ8IRNwbLch2QEzgGEoRC6uGOQ=="], + "@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-hLsgNDzDDO+Tq2YJ9uvmB2PFTYCgeNj6cyAOOBZP9rsOx0kxNBo4/OvDIgYl/cfo7wsoRBxNQ9giYR+rZMcAjA=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], - "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -222,6 +233,8 @@ "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], @@ -230,15 +243,17 @@ "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], + "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -268,7 +283,7 @@ "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], + "effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], @@ -278,6 +293,8 @@ "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -286,6 +303,10 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "file-type": ["file-type@21.3.3", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -302,7 +323,7 @@ "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], - "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -316,13 +337,11 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - "kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "kysely": ["kysely@0.28.14", "", {}, "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -354,8 +373,6 @@ "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], - "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -374,8 +391,6 @@ "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], - "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], @@ -384,6 +399,8 @@ "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], @@ -396,9 +413,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], - - "prisma": ["prisma@7.5.0", "", { "dependencies": { "@prisma/config": "7.5.0", "@prisma/dev": "0.20.0", "@prisma/engines": "7.5.0", "@prisma/studio-core": "0.21.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-n30qZpWehaYQzigLjmuPisyEsvOzHt7bZeRyg8gZ5DvJo9FGjD+gNaY59Ns3hlLD5/jZH5GBeftIss0jDbUoLg=="], + "prisma": ["prisma@7.6.0", "", { "dependencies": { "@prisma/config": "7.6.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.6.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q=="], "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], @@ -416,9 +431,9 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + "remeda": ["remeda@2.33.6", "", {}, "sha512-tazDGH7s75kUPGBKLvhgBEHMgW+TdDFhjUAMdQj57IoWz6HsGa5D2RX5yDUz6IIqiRRvZiaEHzCzWdTeixc/Kg=="], - "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], @@ -432,7 +447,7 @@ "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], - "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -440,6 +455,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -448,7 +465,7 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], @@ -460,9 +477,9 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.8.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.18", "@turbo/darwin-arm64": "2.8.18", "@turbo/linux-64": "2.8.18", "@turbo/linux-arm64": "2.8.18", "@turbo/windows-64": "2.8.18", "@turbo/windows-arm64": "2.8.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-ZO5aYR8wXIcZ+KRL8W7RFWbqIRDyYHtesF/g9zPMndmq+KkX34qrZgG9M+YswJ6BGX4jlYC1FlJjleKENagJdw=="], + "turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], @@ -482,11 +499,13 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@prisma/adapter-pg/@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="], + "@prisma/dev/remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0" } }, "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw=="], + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], + + "@prisma/generator-helper/@prisma/debug": ["@prisma/debug@7.5.0", "", {}, "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg=="], "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], @@ -498,14 +517,6 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "@prisma/adapter-pg/@types/pg/pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "@prisma/adapter-pg/@types/pg/pg-types/postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="], - - "@prisma/adapter-pg/@types/pg/pg-types/postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="], - - "@prisma/adapter-pg/@types/pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], } } diff --git a/package.json b/package.json index 0576122..8cc004d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "devDependencies": { "@biomejs/biome": "^2.4.8", "@types/bun": "^1.3.10", - "turbo": "^2.8.18" + "turbo": "^2.8.18", + "typescript": "^6.0.2" }, "packageManager": "bun@1.3.11" } diff --git a/packages/core/common/internal.ts b/packages/core/common/internal.ts index fb1846e..159d157 100644 --- a/packages/core/common/internal.ts +++ b/packages/core/common/internal.ts @@ -1,6 +1,6 @@ /** * Internal entry point for the core package. - * * Provides access to internal members and objects from `@project-cvsa/core/common` + * * Provides access to internal members and objects from `@cvsa/core/common` * that are intended for internal use only and are not part of the public API. */ diff --git a/packages/core/common/prisma.ts b/packages/core/common/prisma.ts index 2a0f438..d452df8 100644 --- a/packages/core/common/prisma.ts +++ b/packages/core/common/prisma.ts @@ -1,6 +1,6 @@ -import { PrismaClient } from "@project-cvsa/db"; +import { PrismaClient } from "@cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "@project-cvsa/core/common"; +import { env } from "@cvsa/core/common"; const adapter = new PrismaPg({ connectionString: env.DATABASE_URL, diff --git a/packages/core/common/schema/error.ts b/packages/core/common/schema/error.ts index e5d632e..cf9d3b7 100644 --- a/packages/core/common/schema/error.ts +++ b/packages/core/common/schema/error.ts @@ -1,7 +1,22 @@ import { z } from "zod"; +const errorCodes = [ + "NOT_FOUND", + "VALIDATION_ERROR", + "ENTITY_CONFLICT", + "INVALID_CREDENTIALS", + "AUTH_ERROR", + "INTERNAL_SERVER_ERROR", + "USERNAME_IS_ALREADY_TAKEN", + "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", + "INVALID_EMAIL_OR_PASSWORD", + "USER_NOT_FOUND", + "EMAIL_NOT_VERIFIED", + "UNAUTHORIZED", +] as const; + export const ErrorResponseSchema = z.object({ - code: z.string(), + code: z.enum(errorCodes), message: z.string().optional(), }); diff --git a/packages/core/modules/auth/betterAuth.ts b/packages/core/modules/auth/betterAuth.ts index f8ee110..d5ff4ec 100644 --- a/packages/core/modules/auth/betterAuth.ts +++ b/packages/core/modules/auth/betterAuth.ts @@ -1,7 +1,7 @@ import { APIError, betterAuth } from "better-auth"; import { prismaAdapter } from "@better-auth/prisma-adapter"; -import { prisma } from "@project-cvsa/core/common"; -import { getRandomId } from "@project-cvsa/core/common"; +import { prisma } from "@cvsa/core/common"; +import { getRandomId } from "@cvsa/core/common"; import { username } from "better-auth/plugins"; import { bearer } from "better-auth/plugins"; diff --git a/packages/core/modules/auth/user/dto.ts b/packages/core/modules/auth/user/dto.ts index 2be7d4c..06bffaa 100644 --- a/packages/core/modules/auth/user/dto.ts +++ b/packages/core/modules/auth/user/dto.ts @@ -1,7 +1,7 @@ import { z } from "zod"; -import type { auth } from "@project-cvsa/core"; -import { getRandomId } from "@project-cvsa/core/common"; -import type { User } from "@project-cvsa/db"; +import type { auth } from "@cvsa/core"; +import { getRandomId } from "@cvsa/core/common"; +import type { User } from "@cvsa/db"; type BetterAuthUser = Exclude>, null>["user"]; diff --git a/packages/core/modules/catalog/song/dto.ts b/packages/core/modules/catalog/song/dto.ts index 4c511c5..ebf1f2e 100644 --- a/packages/core/modules/catalog/song/dto.ts +++ b/packages/core/modules/catalog/song/dto.ts @@ -1,64 +1,40 @@ import { z } from "zod"; -import type { Song, Singer, Artist, ArtistRole } from "@project-cvsa/db"; - -export const SongTypeSchema = z.enum([ - "ORIGINAL", - "COVER", - "REMIX", - "REMASTER", - "MASHUP", - "INSTRUMENTAL", - "OTHERS", -]); +import { SongSchema, SingerSchema, ArtistSchema, ArtistRoleSchema, SongTypeSchema } from "@cvsa/db"; export type SongId = number; -// Performance DTOs -export const CreatePerformanceSchema = z.object({ - singerId: z.number().int().positive(), - voicebankId: z.number().int().positive().nullable().optional(), - svsEngineId: z.number().int().positive().nullable().optional(), - svsEngineVersionId: z.number().int().positive().nullable().optional(), +const CreateCreationSchema = z.object({ + artistId: z.int().positive(), + roleId: z.int().positive(), }); -export type CreatePerformanceDto = z.infer; - -// Creation DTOs -export const CreateCreationSchema = z.object({ - artistId: z.number().int().positive(), - roleId: z.number().int().positive(), +const CreatePerformanceSchema = z.object({ + singerId: z.int().positive(), + voicebankId: z.int().positive().nullable().optional(), + svsEngineId: z.int().positive().nullable().optional(), + svsEngineVersionId: z.int().positive().nullable().optional(), }); -export type CreateCreationDto = z.infer; - -// Song with relations DTOs -export const createSongSchema = z.object({ +export const CreateSongRequestSchema = z.object({ type: SongTypeSchema.nullable().optional(), name: z.string().nullable().optional(), duration: z.number().nullable().optional(), description: z.string().nullable().optional(), - coverUrl: z.string().url().nullable().optional(), + coverUrl: z.url().nullable().optional(), publishedAt: z.date().nullable().optional(), performances: z.array(CreatePerformanceSchema).optional(), creations: z.array(CreateCreationSchema).optional(), }); -export type CreateSongDto = z.infer; - -export const UpdateSongSchema = z.object({ - type: SongTypeSchema.nullable().optional(), +export const UpdateSongRequestSchema = z.object({ name: z.string().nullable().optional(), - duration: z.number().nullable().optional(), + type: SongTypeSchema.nullable().optional(), + duration: z.int().nullable().optional(), description: z.string().nullable().optional(), - coverUrl: z.string().url().nullable().optional(), - publishedAt: z.date().nullable().optional(), - deletedAt: z.date().nullable().optional(), - performances: z.array(CreatePerformanceSchema).optional(), - creations: z.array(CreateCreationSchema).optional(), + coverUrl: z.url().nullable().optional(), + publishedAt: z.date().nullable().optional() }); -export type UpdateSongDto = z.infer; - export const ListSongsQuerySchema = z.object({ search: z.string().optional(), type: SongTypeSchema.optional(), @@ -66,8 +42,6 @@ export const ListSongsQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(100).optional(), }); -export type ListSongsQueryDto = z.infer; - export const ListSongsResponseSchema = z.object({ songs: z.array( z.object({ @@ -75,11 +49,8 @@ export const ListSongsResponseSchema = z.object({ type: SongTypeSchema.nullable(), name: z.string().nullable(), duration: z.number().nullable(), - description: z.string().nullable(), - coverUrl: z.string().url().nullable(), + coverUrl: z.url().nullable(), publishedAt: z.date().nullable(), - createdAt: z.date(), - updatedAt: z.date(), }) ), total: z.number(), @@ -87,11 +58,23 @@ export const ListSongsResponseSchema = z.object({ limit: z.number(), }); -export type ListSongsResponseDto = z.infer; +export const SongDetailsResponseSchema = z.intersection( + SongSchema, + z.object({ + singers: SingerSchema.array(), + artists: z.intersection(ArtistSchema, z.object({ role: ArtistRoleSchema })).array(), + lyrics: z.array( + z.object({ + plainText: z.string().nullable(), + isTranslated: z.boolean(), + language: z.string().nullable(), + }) + ), + }) +); -export type SongDetailsDto = Song & { - singers: Singer[]; - artists: (Artist & { - role: ArtistRole; - })[]; -}; +export type CreateSongRequestDto = z.infer; +export type UpdateSongRequestDto = z.infer; +export type ListSongsResponseDto = z.infer; +export type ListSongsQueryDto = z.infer; +export type SongDetailsResponseDto = z.infer; diff --git a/packages/core/modules/catalog/song/repository.interface.ts b/packages/core/modules/catalog/song/repository.interface.ts index 292e8da..0d36469 100644 --- a/packages/core/modules/catalog/song/repository.interface.ts +++ b/packages/core/modules/catalog/song/repository.interface.ts @@ -1,14 +1,12 @@ -import type { Song } from "@project-cvsa/db"; -import type { SongDetailsDto } from "./dto"; -import type { CreateSongDto, ListSongsQueryDto, SongId, UpdateSongDto } from "./dto"; +import type { Song } from "@cvsa/db"; +import type { SongDetailsResponseDto } from "./dto"; +import type { CreateSongRequestDto, ListSongsQueryDto, SongId, UpdateSongRequestDto } from "./dto"; export interface ISongRepository { getById(id: SongId): Promise; - getDetailsById(id: SongId): Promise; + getDetailsById(id: SongId): Promise; list(query?: ListSongsQueryDto): Promise<{ songs: Song[]; total: number }>; - create(input: CreateSongDto): Promise; - createWithRelations(input: CreateSongDto): Promise; - update(id: SongId, input: UpdateSongDto): Promise; - updateWithRelations(id: SongId, input: UpdateSongDto): Promise; + create(input: CreateSongRequestDto): Promise; + update(id: SongId, input: UpdateSongRequestDto): Promise; softDelete(id: SongId): Promise; } diff --git a/packages/core/modules/catalog/song/repository.ts b/packages/core/modules/catalog/song/repository.ts index 96910fb..061717a 100644 --- a/packages/core/modules/catalog/song/repository.ts +++ b/packages/core/modules/catalog/song/repository.ts @@ -1,6 +1,6 @@ -import type { Prisma, PrismaClient } from "@project-cvsa/db"; -import type { SongDetailsDto } from "./dto"; -import type { CreateSongDto, ListSongsQueryDto, SongId, UpdateSongDto } from "./dto"; +import type { Prisma, PrismaClient } from "@cvsa/db"; +import type { SongDetailsResponseDto } from "./dto"; +import type { CreateSongRequestDto, ListSongsQueryDto, SongId, UpdateSongRequestDto } from "./dto"; import type { ISongRepository } from "./repository.interface"; export class SongRepository implements ISongRepository { @@ -10,7 +10,7 @@ export class SongRepository implements ISongRepository { return this.prisma.song.findFirst({ where: { id, deletedAt: null } }); } - async getDetailsById(id: SongId): Promise { + async getDetailsById(id: SongId): Promise { const data = await this.prisma.song.findFirst({ where: { id, deletedAt: null }, include: { @@ -25,10 +25,11 @@ export class SongRepository implements ISongRepository { artist: true, }, }, + lyrics: true, }, }); if (!data) return null; - const { performances, creations, ...song } = data; + const { performances, creations, lyrics, ...song } = data; return { singers: performances.map((item) => item.singer), artists: creations.map((item) => { @@ -37,6 +38,13 @@ export class SongRepository implements ISongRepository { role: item.role, }; }), + lyrics: lyrics.map((item) => { + return { + plainText: item.plainText, + language: item.language, + isTranslated: item.isTranslated, + }; + }), ...song, }; } @@ -69,20 +77,7 @@ export class SongRepository implements ISongRepository { return { songs, total }; } - async create(input: CreateSongDto) { - return await this.prisma.song.create({ - data: { - type: input.type ?? null, - name: input.name ?? null, - duration: input.duration ?? null, - description: input.description ?? null, - coverUrl: input.coverUrl ?? null, - publishedAt: input.publishedAt ?? null, - }, - }); - } - - async createWithRelations(input: CreateSongDto) { + async create(input: CreateSongRequestDto) { const { performances, creations, ...songData } = input; return this.prisma.song.create({ @@ -93,46 +88,21 @@ export class SongRepository implements ISongRepository { description: songData.description ?? null, coverUrl: songData.coverUrl ?? null, publishedAt: songData.publishedAt ?? null, - performances: performances - ? { - create: performances.map((p) => ({ - singerId: p.singerId, - voicebankId: p.voicebankId ?? undefined, - svsEngineId: p.svsEngineId ?? undefined, - svsEngineVersionId: p.svsEngineVersionId ?? undefined, - })), - } - : undefined, - creations: creations - ? { - create: creations.map((c) => ({ - artistId: c.artistId, - roleId: c.roleId, - artistRoleId: c.roleId, - })), - } - : undefined, - }, - }); - } - - async update(id: SongId, input: UpdateSongDto) { - return this.prisma.song.update({ - where: { id }, - data: { - ...(input.type !== undefined ? { type: input.type } : {}), - ...(input.name !== undefined ? { name: input.name } : {}), - ...(input.duration !== undefined ? { duration: input.duration } : {}), - ...(input.description !== undefined ? { description: input.description } : {}), - ...(input.coverUrl !== undefined ? { coverUrl: input.coverUrl } : {}), - ...(input.publishedAt !== undefined ? { publishedAt: input.publishedAt } : {}), - ...(input.deletedAt !== undefined ? { deletedAt: input.deletedAt } : {}), + performances: performances && { + create: performances, + }, + creations: creations && { + create: creations.map((c) => ({ + artistId: c.artistId, + artistRoleId: c.roleId, + })), + }, }, }); } - async updateWithRelations(id: SongId, input: UpdateSongDto) { - const { performances, creations, ...songData } = input; + async update(id: SongId, input: UpdateSongRequestDto) { + const songData = input; const data: Prisma.SongUpdateInput = { ...(songData.type !== undefined ? { type: songData.type } : {}), @@ -141,32 +111,8 @@ export class SongRepository implements ISongRepository { ...(songData.description !== undefined ? { description: songData.description } : {}), ...(songData.coverUrl !== undefined ? { coverUrl: songData.coverUrl } : {}), ...(songData.publishedAt !== undefined ? { publishedAt: songData.publishedAt } : {}), - ...(songData.deletedAt !== undefined ? { deletedAt: songData.deletedAt } : {}), }; - if (performances !== undefined) { - data.performances = { - deleteMany: {}, - create: performances.map((p) => ({ - singerId: p.singerId, - voicebankId: p.voicebankId ?? undefined, - svsEngineId: p.svsEngineId ?? undefined, - svsEngineVersionId: p.svsEngineVersionId ?? undefined, - })), - }; - } - - if (creations !== undefined) { - data.creations = { - deleteMany: {}, - create: creations.map((c) => ({ - artistId: c.artistId, - roleId: c.roleId, - artistRoleId: c.roleId, - })), - }; - } - return this.prisma.song.update({ where: { id }, data, diff --git a/packages/core/modules/catalog/song/service.ts b/packages/core/modules/catalog/song/service.ts index 8e60d79..e227dbc 100644 --- a/packages/core/modules/catalog/song/service.ts +++ b/packages/core/modules/catalog/song/service.ts @@ -1,14 +1,20 @@ -import { prisma } from "@project-cvsa/core/common"; -import type { SongDetailsDto, SongId, ListSongsQueryDto, ListSongsResponseDto } from "./dto"; -import type { CreateSongDto, UpdateSongDto } from "./dto"; +import { prisma } from "@cvsa/core/common"; +import type { + SongDetailsResponseDto, + SongId, + ListSongsQueryDto, + ListSongsResponseDto, +} from "./dto"; +import type { CreateSongRequestDto, UpdateSongRequestDto } from "./dto"; import { SongRepository } from "./repository"; -import { AppError } from "@project-cvsa/core"; -import type { Song } from "@project-cvsa/db"; +import { AppError } from "@cvsa/core"; +import type { Song } from "@cvsa/db"; +import { pick, map } from "remeda"; export class SongService { constructor(private readonly repository: SongRepository = new SongRepository(prisma)) {} - async getDetails(id: SongId): Promise { + async getDetails(id: SongId): Promise { const result = await this.repository.getDetailsById(id); if (result === null) { throw new AppError("Song not found", "NOT_FOUND", 404); @@ -16,16 +22,16 @@ export class SongService { return result; } - async create(input: CreateSongDto): Promise { - return this.repository.createWithRelations(input); + async create(input: CreateSongRequestDto): Promise { + return this.repository.create(input); } - async update(id: SongId, input: UpdateSongDto): Promise { + async update(id: SongId, input: UpdateSongRequestDto): Promise { const existing = await this.repository.getById(id); if (existing === null) { throw new AppError("Song not found", "NOT_FOUND", 404); } - return this.repository.updateWithRelations(id, input); + return this.repository.update(id, input); } async delete(id: SongId): Promise { @@ -40,7 +46,10 @@ export class SongService { const { offset = 0, limit = 50 } = query; const result = await this.repository.list(query); return { - songs: result.songs, + songs: map( + result.songs, + pick(["id", "type", "name", "duration", "coverUrl", "publishedAt"]) + ), total: result.total, offset, limit, diff --git a/packages/core/package.json b/packages/core/package.json index b7cbd19..d2ae194 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@project-cvsa/core", + "name": "@cvsa/core", "type": "module", "module": "index.ts", "scripts": { @@ -13,10 +13,7 @@ "devDependencies": { "@prisma/adapter-pg": "^7.4.1", "@types/bun": "latest", - "@project-cvsa/typescript-config": "workspace:*" - }, - "peerDependencies": { - "typescript": "^5" + "@cvsa/typescript-config": "workspace:*" }, "exports": { ".": "./index.ts", @@ -24,10 +21,11 @@ }, "dependencies": { "@better-auth/prisma-adapter": "^1.5.5", - "@project-cvsa/db": "*", + "@cvsa/db": "*", "@t3-oss/env-core": "^0.13.10", "better-auth": "^1.5.5", "nanoid": "^5.1.6", + "remeda": "^2.33.6", "zod": "^4.3.6" } } diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index 02004bd..f321ed3 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -1,9 +1,9 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; -import { PrismaClient } from "@project-cvsa/db"; +import { PrismaClient } from "@cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; -import { SongRepository } from "@project-cvsa/core"; -import type { CreateSongDto, UpdateSongDto } from "@project-cvsa/core"; -import { env } from "@project-cvsa/core/common"; +import { SongRepository } from "@cvsa/core"; +import type { CreateSongRequestDto, UpdateSongRequestDto } from "@cvsa/core"; +import { env } from "@cvsa/core/common"; const prisma = new PrismaClient({ adapter: new PrismaPg({ @@ -29,7 +29,7 @@ describe("SongRepository Integration Tests", () => { describe("create", () => { test("should create a song with all fields", async () => { - const input: CreateSongDto = { + const input: CreateSongRequestDto = { type: "ORIGINAL", name: "Test Song", duration: 180, @@ -51,7 +51,7 @@ describe("SongRepository Integration Tests", () => { }); test("should create a song with minimal fields", async () => { - const input: CreateSongDto = { + const input: CreateSongRequestDto = { name: "Minimal Song", }; @@ -146,7 +146,7 @@ describe("SongRepository Integration Tests", () => { test("should update all fields", async () => { const created = await repository.create({ name: "Old Name", type: "ORIGINAL" }); - const input: UpdateSongDto = { + const input: UpdateSongRequestDto = { name: "New Name", type: "COVER", duration: 200, diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index 1550500..50cf6c6 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, mock } from "bun:test"; -import { SongService, AppError } from "@project-cvsa/core"; -import type { SongRepository, SongDetailsDto } from "@project-cvsa/core"; +import { SongService, AppError } from "@cvsa/core"; +import type { SongRepository, SongDetailsResponseDto } from "@cvsa/core"; describe("SongService", () => { const mockGetDetailsById = mock(); @@ -9,7 +9,7 @@ describe("SongService", () => { } as unknown as SongRepository; const songService = new SongService(mockRepository); - const mockSongDetails: SongDetailsDto = { + const mockSongDetails: SongDetailsResponseDto = { id: 1, type: "ORIGINAL", name: "Test Song", @@ -30,6 +30,7 @@ describe("SongService", () => { moegirlId: null, singers: [], artists: [], + lyrics: [] }; describe("getDetails", () => { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 7f43608..ee22f73 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "@project-cvsa/typescript-config/base.json", + "extends": "@cvsa/typescript-config/base.json", "include": ["modules/**/*.ts", "tests/**/*.ts", "common/**/*.ts", "index.ts"] } diff --git a/packages/db/.gitignore b/packages/db/.gitignore index b50a6d8..9ff5e3f 100644 --- a/packages/db/.gitignore +++ b/packages/db/.gitignore @@ -1 +1,2 @@ prisma/generated +prisma/zod diff --git a/packages/db/README.md b/packages/db/README.md index 5c6b150..d72b5b2 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -1,4 +1,4 @@ -# @project-cvsa/db +# @cvsa/db ## Available Scripts diff --git a/packages/db/index.ts b/packages/db/index.ts index c792826..ce4ab2f 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -1,3 +1,4 @@ export * from "./prisma/generated/client"; export * from "./prisma/generated/models"; export * from "./prisma/generated/enums"; +export * from "./prisma/zod"; diff --git a/packages/db/package.json b/packages/db/package.json index bf7221f..4d8504d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,5 +1,5 @@ { - "name": "@project-cvsa/db", + "name": "@cvsa/db", "type": "module", "module": "index.ts", "scripts": { @@ -13,16 +13,15 @@ "typecheck": "bunx --bun tsc --noEmit" }, "devDependencies": { - "prisma": "^7.4.1", - "prisma-json-types-generator": "^4.1.1", - "@project-cvsa/typescript-config": "workspace:*" - }, - "peerDependencies": { - "typescript": "^5" + "@cvsa/prisma-zod": "^3.3.0", + "@cvsa/typescript-config": "workspace:*", + "prisma": "^7.6.0", + "prisma-json-types-generator": "^4.1.1" }, "dependencies": { - "@prisma/adapter-pg": "^7.4.1", - "@prisma/client": "^7.4.1", - "@types/pg": "^8.16.0" + "@prisma/adapter-pg": "^7.6.0", + "@prisma/client": "^7.6.0", + "@types/pg": "^8.16.0", + "zod": "^4.3.6" } } diff --git a/packages/db/prisma/migrations/20260227101459_init/migration.sql b/packages/db/prisma/migrations/20260227101459_init/migration.sql deleted file mode 100644 index e86dee7..0000000 --- a/packages/db/prisma/migrations/20260227101459_init/migration.sql +++ /dev/null @@ -1,514 +0,0 @@ --- CreateSchema -CREATE SCHEMA IF NOT EXISTS "auth"; - --- CreateSchema -CREATE SCHEMA IF NOT EXISTS "core"; - --- CreateSchema -CREATE SCHEMA IF NOT EXISTS "meta"; - --- CreateSchema -CREATE SCHEMA IF NOT EXISTS "platform"; - --- CreateEnum -CREATE TYPE "core"."third_party_platform" AS ENUM ('YOUTUBE', 'NICONICO', 'BILIBILI', 'VOCALOID_WIKI', 'VOCALOID_LYRICS_WIKI', 'SOUNDCLOUD', 'NETEASE_MUSIC', 'QQ_MUSIC', 'FIVE_SING', 'KUGOU', 'SPOTIFY', 'APPLE_MUSIC', 'YOUTUBE_MUSIC', 'WIKIPEDIA', 'BAIDU_BAIKE', 'PIXIV', 'WEIBO', 'TWITTER', 'VOCADB', 'MOEGIRLPEDIA', 'MUSICBRAINZ', 'VCPEDIA', 'XIAOHONGSHU'); - --- CreateEnum -CREATE TYPE "core"."song_type" AS ENUM ('ORIGINAL', 'COVER', 'REMIX', 'REMASTER', 'MASHUP', 'INSTRUMENTAL', 'OTHERS'); - --- CreateEnum -CREATE TYPE "meta"."change_type" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'RESTORE'); - --- CreateTable -CREATE TABLE "auth"."session" ( - "id" TEXT NOT NULL, - "user_id" TEXT NOT NULL, - "ip_address" TEXT, - "user_agent" TEXT, - "secret_hash" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "last_verified_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "session_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "auth"."user" ( - "id" TEXT NOT NULL, - "username" TEXT NOT NULL, - "display_name" TEXT, - "email" TEXT, - "password" TEXT NOT NULL, - "role_id" INTEGER, - "reputation" INTEGER NOT NULL DEFAULT 0, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "user_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."album" ( - "id" SERIAL NOT NULL, - "name" TEXT, - "localized_names" JSONB, - "description" TEXT, - "localized_descriptions" JSONB, - "cover_url" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "album_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."artist" ( - "id" SERIAL NOT NULL, - "name" TEXT, - "localized_names" JSONB, - "aliases" TEXT[], - "description" TEXT, - "localized_descriptions" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "user_id" TEXT, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "artist_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."artist_role" ( - "id" SERIAL NOT NULL, - "role" TEXT NOT NULL, - "localized_roles" JSONB, - "song_id" INTEGER NOT NULL, - "artist_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "artist_role_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."external_link" ( - "id" SERIAL NOT NULL, - "label" TEXT, - "url" TEXT NOT NULL, - "platform" "core"."third_party_platform", - "platform_id" TEXT, - "song_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "external_link_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."group" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "group_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."lyrics" ( - "id" SERIAL NOT NULL, - "song_id" INTEGER NOT NULL, - "language" TEXT, - "plain_text" TEXT, - "ttml" TEXT, - "lrc" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "lyrics_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."singer" ( - "id" SERIAL NOT NULL, - "name" TEXT, - "avatar_url" TEXT, - "localized_names" JSONB, - "description" TEXT, - "localized_descriptions" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "singer_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."song" ( - "id" SERIAL NOT NULL, - "type" "core"."song_type", - "original_song_id" INTEGER, - "name" TEXT, - "duration" INTEGER, - "localized_names" JSONB, - "description" TEXT, - "localized_descriptions" JSONB, - "bilibili_aid" BIGINT, - "bilibili_bvid" TEXT, - "vocadb_id" INTEGER, - "vcpedia_id" INTEGER, - "moegirl_id" INTEGER, - "cover_url" TEXT, - "published_at" TIMESTAMP(3), - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "song_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."song_series" ( - "id" SERIAL NOT NULL, - "name" TEXT, - "localized_names" JSONB, - "description" TEXT, - "localized_descriptions" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "song_series_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."svs_engine" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "localized_descriptions" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "svs_engine_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."svs_engine_version" ( - "id" SERIAL NOT NULL, - "version_string" TEXT NOT NULL, - "svs_engine_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "svs_engine_version_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."tag" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "localized_names" JSONB, - "parent_id" INTEGER, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "tag_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."voicebank" ( - "id" SERIAL NOT NULL, - "description" TEXT, - "localized_descriptions" JSONB, - "singer_id" INTEGER NOT NULL, - "language" TEXT NOT NULL, - "svs_engine_version_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "voicebank_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "meta"."history" ( - "id" SERIAL NOT NULL, - "user_id" TEXT NOT NULL, - "table" TEXT NOT NULL, - "object_id" INTEGER NOT NULL, - "type" "meta"."change_type" NOT NULL, - "comment" TEXT, - "old" JSONB, - "new" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "history_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "meta"."permission" ( - "id" SERIAL NOT NULL, - "action" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "permission_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "meta"."role" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "localized_names" JSONB, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "role_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "platform"."file" ( - "id" TEXT NOT NULL, - "original_name" TEXT, - "path" TEXT, - "user_id" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "file_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "platform"."post" ( - "id" TEXT NOT NULL, - "text" TEXT, - "user_id" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "post_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "platform"."reputation_history" ( - "id" SERIAL NOT NULL, - "user_id" TEXT NOT NULL, - "action_type" TEXT NOT NULL, - "points" INTEGER NOT NULL, - "source_type" INTEGER NOT NULL, - "source_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "reputation_history_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."singer_of_song" ( - "id" SERIAL NOT NULL, - "song_id" INTEGER NOT NULL, - "singer_id" INTEGER NOT NULL, - "voicebank_id" INTEGER, - "svs_engine_id" INTEGER, - "svs_engine_version_id" INTEGER, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "singer_of_song_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."singer_svs_engine" ( - "id" SERIAL NOT NULL, - "singer_id" INTEGER NOT NULL, - "svs_engine_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "singer_svs_engine_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."singer_svs_engine_version" ( - "id" SERIAL NOT NULL, - "singer_id" INTEGER NOT NULL, - "svs_engine_version_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "singer_svs_engine_version_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."song_in_album" ( - "id" SERIAL NOT NULL, - "song_id" INTEGER NOT NULL, - "album_id" INTEGER NOT NULL, - "track_number" INTEGER NOT NULL, - "disc_number" INTEGER NOT NULL DEFAULT 1, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "song_in_album_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."song_in_series" ( - "id" SERIAL NOT NULL, - "song_id" INTEGER NOT NULL, - "series_id" INTEGER NOT NULL, - "order" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "song_in_series_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "core"."song_tag" ( - "id" SERIAL NOT NULL, - "song_id" INTEGER NOT NULL, - "tag_id" INTEGER NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3) NOT NULL, - "deleted_at" TIMESTAMP(3), - - CONSTRAINT "song_tag_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "meta"."role_permission" ( - "id" SERIAL NOT NULL, - "role_id" INTEGER NOT NULL, - "permission_id" INTEGER NOT NULL, - - CONSTRAINT "role_permission_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "user_username_key" ON "auth"."user"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "user_email_key" ON "auth"."user"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "artist_user_id_key" ON "core"."artist"("user_id"); - --- CreateIndex -CREATE UNIQUE INDEX "role_name_key" ON "meta"."role"("name"); - --- AddForeignKey -ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "auth"."user" ADD CONSTRAINT "user_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "meta"."role"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."artist" ADD CONSTRAINT "artist_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."artist_role" ADD CONSTRAINT "artist_role_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."artist_role" ADD CONSTRAINT "artist_role_artist_id_fkey" FOREIGN KEY ("artist_id") REFERENCES "core"."artist"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."external_link" ADD CONSTRAINT "external_link_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."lyrics" ADD CONSTRAINT "lyrics_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song" ADD CONSTRAINT "song_original_song_id_fkey" FOREIGN KEY ("original_song_id") REFERENCES "core"."song"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."svs_engine_version" ADD CONSTRAINT "svs_engine_version_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."tag" ADD CONSTRAINT "tag_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "core"."tag"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."voicebank" ADD CONSTRAINT "voicebank_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."voicebank" ADD CONSTRAINT "voicebank_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "meta"."history" ADD CONSTRAINT "history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "platform"."file" ADD CONSTRAINT "file_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "platform"."post" ADD CONSTRAINT "post_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "platform"."reputation_history" ADD CONSTRAINT "reputation_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_voicebank_id_fkey" FOREIGN KEY ("voicebank_id") REFERENCES "core"."voicebank"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_svs_engine" ADD CONSTRAINT "singer_svs_engine_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_svs_engine" ADD CONSTRAINT "singer_svs_engine_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_svs_engine_version" ADD CONSTRAINT "singer_svs_engine_version_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."singer_svs_engine_version" ADD CONSTRAINT "singer_svs_engine_version_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_in_album" ADD CONSTRAINT "song_in_album_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_in_album" ADD CONSTRAINT "song_in_album_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "core"."album"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_in_series" ADD CONSTRAINT "song_in_series_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_in_series" ADD CONSTRAINT "song_in_series_series_id_fkey" FOREIGN KEY ("series_id") REFERENCES "core"."song_series"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_tag" ADD CONSTRAINT "song_tag_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "core"."song_tag" ADD CONSTRAINT "song_tag_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "core"."tag"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "meta"."role_permission" ADD CONSTRAINT "role_permission_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "meta"."role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "meta"."role_permission" ADD CONSTRAINT "role_permission_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "meta"."permission"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql b/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql deleted file mode 100644 index 4e1ea4e..0000000 --- a/packages/db/prisma/migrations/20260310103700_better_auth/migration.sql +++ /dev/null @@ -1,80 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `last_verified_at` on the `session` table. All the data in the column will be lost. - - You are about to drop the column `secret_hash` on the `session` table. All the data in the column will be lost. - - You are about to drop the column `deleted_at` on the `user` table. All the data in the column will be lost. - - You are about to drop the column `display_name` on the `user` table. All the data in the column will be lost. - - You are about to drop the column `password` on the `user` table. All the data in the column will be lost. - - A unique constraint covering the columns `[token]` on the table `session` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[display_username]` on the table `user` will be added. If there are existing duplicate values, this will fail. - - Added the required column `expiresAt` to the `session` table without a default value. This is not possible if the table is not empty. - - Added the required column `token` to the `session` table without a default value. This is not possible if the table is not empty. - - Added the required column `updated_at` to the `session` table without a default value. This is not possible if the table is not empty. - - Added the required column `display_username` to the `user` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "auth"."session" DROP COLUMN "last_verified_at", -DROP COLUMN "secret_hash", -ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL, -ADD COLUMN "token" TEXT NOT NULL, -ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL; - --- AlterTable -ALTER TABLE "auth"."user" DROP COLUMN "deleted_at", -DROP COLUMN "display_name", -DROP COLUMN "password", -ADD COLUMN "display_username" TEXT NOT NULL, -ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false, -ADD COLUMN "image" TEXT, -ADD COLUMN "name" TEXT; - --- CreateTable -CREATE TABLE "auth"."account" ( - "id" TEXT NOT NULL, - "accountId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "accessToken" TEXT, - "refreshToken" TEXT, - "idToken" TEXT, - "accessTokenExpiresAt" TIMESTAMP(3), - "refreshTokenExpiresAt" TIMESTAMP(3), - "scope" TEXT, - "password" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "account_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "auth"."verification" ( - "id" TEXT NOT NULL, - "identifier" TEXT NOT NULL, - "value" TEXT NOT NULL, - "expiresAt" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "verification_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "account_userId_idx" ON "auth"."account"("userId"); - --- CreateIndex -CREATE INDEX "verification_identifier_idx" ON "auth"."verification"("identifier"); - --- CreateIndex -CREATE INDEX "session_user_id_idx" ON "auth"."session"("user_id"); - --- CreateIndex -CREATE UNIQUE INDEX "session_token_key" ON "auth"."session"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "user_display_username_key" ON "auth"."user"("display_username"); - --- AddForeignKey -ALTER TABLE "auth"."account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "auth"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml deleted file mode 100644 index 044d57c..0000000 --- a/packages/db/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/packages/db/prisma/models/core/lyrics.prisma b/packages/db/prisma/models/core/lyrics.prisma index 7ee4257..337c975 100644 --- a/packages/db/prisma/models/core/lyrics.prisma +++ b/packages/db/prisma/models/core/lyrics.prisma @@ -1,14 +1,15 @@ model Lyrics { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - language String? - plainText String? @map("plain_text") - ttml String? - lrc String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + language String? + isTranslated Boolean @default(false) @map("is_translated") + plainText String? @map("plain_text") + ttml String? + lrc String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("lyrics") @@schema("core") diff --git a/packages/db/prisma/relations/core/creation.prisma b/packages/db/prisma/relations/core/creation.prisma index f73f9b3..349dbae 100644 --- a/packages/db/prisma/relations/core/creation.prisma +++ b/packages/db/prisma/relations/core/creation.prisma @@ -5,11 +5,10 @@ model Creation { artist Artist @relation(fields: [artistId], references: [id]) artistId Int @map("artist_id") role ArtistRole @relation(fields: [artistRoleId], references: [id]) - roleId Int @map("role_id") + artistRoleId Int @map("artist_role_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - artistRoleId Int @@map("creation") @@schema("core") diff --git a/packages/db/prisma/models/core/performance.prisma b/packages/db/prisma/relations/core/performance.prisma similarity index 100% rename from packages/db/prisma/models/core/performance.prisma rename to packages/db/prisma/relations/core/performance.prisma diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 34645f1..b9618ee 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -12,7 +12,12 @@ datasource db { schemas = ["core", "meta", "platform", "auth"] } -/* +generator zod { + provider = "prisma-zod" + output = "./zod" +} + +/* # CVSA (Chinese Vocal Synthesis Archive) Database Schema ## Overview @@ -27,9 +32,9 @@ The database is partitioned into several PostgreSQL schemas. ### A. [core] - The Encyclopedia This is the heart of the project. It stores universal SVS data that remains objective across platforms. -- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), +- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), SVS Engines (Vocaloid, Synthesizer V), Voicebanks, Albums, and Tags. -- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata +- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata (Simplified Chinese, Traditional Chinese, Japanese, English, etc.). - Versioning: Supports tracking of specific SVS engine versions and voicebank iterations. @@ -44,17 +49,17 @@ Handles user accounts, session persistence, and secure authentication. ### D. [meta] - Governance & Auditing Manages the "data about data." -- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) +- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) in the `core` schema for accountability and rollbacks. - Permissions: Granular action-based permissions assigned to roles. ## 3. Technical Constraints & Design Patterns - Soft Deletion: Most core entities implement a `deletedAt` timestamp for soft-deletion support. -- Modular Relations: Many-to-Many relations are extracted into separate files +- Modular Relations: Many-to-Many relations are extracted into separate files within the `/relations` directory to keep entity definitions clean and prevent Prisma file bloat. ## 4. Current Status & TODO - Indexing: Performance indexes and unique constraints beyond primary keys are still under design. - Constraints: Complex business logic constraints (e.g., preventing circular tag parents) are handled at the application layer. -*/ \ No newline at end of file +*/ diff --git a/packages/db/schema.xml b/packages/db/schema.xml new file mode 100644 index 0000000..a472bc8 --- /dev/null +++ b/packages/db/schema.xml @@ -0,0 +1,955 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files (if enabled) +5. Multiple file entries, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + +prisma/ + models/ + auth/ + account.prisma + README.md + session.prisma + user.prisma + verification.prisma + core/ + album.prisma + artist.prisma + artistRole.prisma + externalLink.prisma + group.prisma + lyrics.prisma + README.md + singer.prisma + song.prisma + songSeries.prisma + svsEngine.prisma + svsEngineVersion.prisma + tag.prisma + voicebank.prisma + meta/ + history.prisma + permission.prisma + README.md + role.prisma + platform/ + file.prisma + post.prisma + README.md + reputationHistory.prisma + models.prisma + relations/ + core/ + creation.prisma + performance.prisma + songInAlbum.prisma + songInSeries.prisma + songTag.prisma + meta/ + rolePermission.prisma + README.md + relations.prisma + schema.prisma + types.ts +.env.example +.gitignore +index.ts +package.json +prisma.config.ts +README.md +tsconfig.json + + + +This section contains the contents of the repository's files. + + +model Account { + id String @id + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("account") + @@schema("auth") +} + + + +# Auth Schema + +This schema handles Identity and Access Management (IAM). + +- **User:** The central account entity. +- **Session:** Tracks active logins, device information. Used in authorization. + + + +model Session { + id String @id + token String + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + expiresAt DateTime + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([token]) + @@index([userId]) + @@map("session") + @@schema("auth") +} + + + +model User { + id String @id + name String? + username String @unique + displayUsername String @unique @map("display_username") + email String? @unique + emailVerified Boolean @default(false) @map("email_verified") + image String? + changesMade History[] + role Role? @relation(fields: [roleId], references: [id]) + roleId Int? @map("role_id") + sessions Session[] + accounts Account[] + artist Artist? + reputation Int @default(0) + reputationHistories ReputationHistory[] + posts Post[] + files File[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("user") + @@schema("auth") +} + + + +model Verification { + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([identifier]) + @@map("verification") + @@schema("auth") +} + + + +model Album { + id Int @id @default(autoincrement()) + name String? + /// [LocalizedField] + localizedNames Json? @map("localized_names") + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + songInAlbums SongInAlbum[] + coverUrl String? @map("cover_url") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("album") + @@schema("core") +} + + + +model Artist { + id Int @id @default(autoincrement()) + name String? + /// [LocalizedField] + localizedNames Json? @map("localized_names") + aliases String[] + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + creations Creation[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + user User? @relation(fields: [userId], references: [id]) + userId String? @unique @map("user_id") + deletedAt DateTime? @map("deleted_at") + + @@map("artist") + @@schema("core") +} + + + +model ArtistRole { + id Int @id @default(autoincrement()) + role String + /// [LocalizedField] + localizedRoles Json? @map("localized_roles") + creations Creation[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("artist_role") + @@schema("core") +} + + + +model ExternalLink { + id Int @id @default(autoincrement()) + label String? + url String + platform Platform? + platformId String? @map("platform_id") + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("external_link") + @@schema("core") +} + +enum Platform { + YOUTUBE + NICONICO + BILIBILI + VOCALOID_WIKI + VOCALOID_LYRICS_WIKI + SOUNDCLOUD + NETEASE_MUSIC + QQ_MUSIC + FIVE_SING // 5sing.kugou.com + KUGOU + SPOTIFY + APPLE_MUSIC + YOUTUBE_MUSIC + WIKIPEDIA + BAIDU_BAIKE + PIXIV + WEIBO + TWITTER + VOCADB + MOEGIRLPEDIA + MUSICBRAINZ + VCPEDIA + XIAOHONGSHU // aka RedNote + + @@map("third_party_platform") + @@schema("core") +} + + + +model Group { + id Int @id @default(autoincrement()) + name String + description String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("group") + @@schema("core") +} + + + +model Lyrics { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + language String? + isTranslated Boolean @default(false) @map("is_translated") + plainText String? @map("plain_text") + ttml String? + lrc String? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("lyrics") + @@schema("core") +} + + + +# Core Schema + +The `core` schema is the heart of the CVSA database. It functions as a structured encyclopedia for the Chinese Singing Voice Synthesis community. + +## Design Philosophy + +The data here is designed to be **objective and universal**. It models the relationships between music, creators, etc. + +## Sub-domains + +1. **Music Entities:** Songs, Albums, Lyrics, and Song Series. +2. **Talent:** Artists (producers/illustrators) and Singers (virtual characters). +3. **Classification:** A hierarchical Tag system for genre and thematic categorization. + +## Localization + +Most entities use `LocalizedField` (JSON) to support multi-language names and descriptions (e.g., Chinese, Japanese, and English). + + + +model Singer { + id Int @id @default(autoincrement()) + name String? + avatarUrl String? @map("avatar_url") + /// [LocalizedField] + localizedNames Json? @map("localized_names") + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + voicebanks Voicebank[] + performances Performance[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("singer") + @@schema("core") +} + + + +model Song { + id Int @id @default(autoincrement()) + type SongType? + originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id]) + originalSongId Int? @map("original_song_id") + derivations Song[] @relation(name: "originalSong") + name String? + duration Int? + lyrics Lyrics[] + songTags SongTag[] + /// [LocalizedField] + localizedNames Json? @map("localized_names") + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + songInAlbums SongInAlbum[] + bilibiliAid BigInt? @map("bilibili_aid") + bilibiliBvid String? @map("bilibili_bvid") + vocadbId Int? @map("vocadb_id") + vcpediaId Int? @map("vcpedia_id") + moegirlId Int? @map("moegirl_id") + externalLinks ExternalLink[] + coverUrl String? @map("cover_url") + creations Creation[] + performances Performance[] + songInSeries SongInSeries[] + publishedAt DateTime? @map("published_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("song") + @@schema("core") +} + +enum SongType { + ORIGINAL + COVER + REMIX + REMASTER + MASHUP + INSTRUMENTAL + OTHERS + + @@map("song_type") + @@schema("core") +} + + + +model SongSeries { + id Int @id @default(autoincrement()) + name String? + /// [LocalizedField] + localizedNames Json? @map("localized_names") + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + songInSeries SongInSeries[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("song_series") + @@schema("core") +} + + + +model SvsEngine { + id Int @id @default(autoincrement()) + name String + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + svsEngineVersions SvsEngineVersion[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + performances Performance[] + + @@map("svs_engine") + @@schema("core") +} + + + +model SvsEngineVersion { + id Int @id @default(autoincrement()) + versionString String @map("version_string") + svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) + svsEngineId Int @map("svs_engine_id") + voicebanks Voicebank[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + performances Performance[] + + @@map("svs_engine_version") + @@schema("core") +} + + + +model Tag { + id Int @id @default(autoincrement()) + name String + /// [LocalizedField] + localizedNames Json? @map("localized_names") + songTags SongTag[] + parent Tag? @relation(name: "tagTree", fields: [parentId], references: [id]) + parentId Int? @map("parent_id") + children Tag[] @relation(name: "tagTree") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("tag") + @@schema("core") +} + + + +model Voicebank { + id Int @id @default(autoincrement()) + singer Singer @relation(fields: [singerId], references: [id]) + description String? + /// [LocalizedField] + localizedDescriptions Json? @map("localized_descriptions") + singerId Int @map("singer_id") + language String + svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) + svsEngineVersionId Int @map("svs_engine_version_id") + performances Performance[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("voicebank") + @@schema("core") +} + + + +model History { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + table String + objectId Int @map("object_id") + type ChangeType + comment String? + old Json? + new Json? + createdAt DateTime @default(now()) @map("created_at") + + @@map("history") + @@schema("meta") +} + +enum ChangeType { + CREATE + UPDATE + DELETE + RESTORE + + @@map("change_type") + @@schema("meta") +} + + + +model Permission { + id Int @id @default(autoincrement()) + action String + roles RolePermission[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("permission") + @@schema("meta") +} + + + +# Meta Schema + +The `meta` schema manages system governance, auditing, and configuration. + +- **Auditing:** The `History` table tracks every change made to the `core` entities (Create/Update/Delete) for rollback and accountability. +- **RBAC:** Implements Role-Based Access Control via `Role` and `Permission` entities. + + + +model Role { + id Int @id @default(autoincrement()) + name String @unique + /// [LocalizedField] + localizedNames Json? @map("localized_names") + permissions RolePermission[] + users User[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("role") + @@schema("meta") +} + + + +model File { + id String @id + originalName String? @map("original_name") + path String? + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + + @@map("file") + @@schema("platform") +} + + + +model Post { + id String @id + text String? + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("post") + @@schema("platform") +} + + + +# Platform Schema + +This schema contains tables and logic specific to the **CVSA as a platform**. + +## Purpose + +While the `core` schema focuses on universal SVS (Singing Voice Synthesis) data that could theoretically be shared across different encyclopedia projects, the `platform` schema manages data unique to this specific implementation. + +It includes: + +- **User Activity:** Personal notes, bookmarks, and interaction history. +- **System State:** File storage metadata and upload tracking. + +and so on. + + + +model ReputationHistory { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId String @map("user_id") + actionType String @map("action_type") + points Int + sourceType Int @map("source_type") + sourceId Int @map("source_id") + createdAt DateTime @default(now()) @map("created_at") + + @@map("reputation_history") + @@schema("platform") +} + + + +/* +NOTE: +This is an empty placeholder file to ensure the Prisma Language Server functions +correctly, serving as a temporary workaround. + +See the link below for details: +https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600 + +This issue persists as of Prisma v7.3.0, which was the latest +version at the time of writing. +*/ + + + +model Creation { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + artist Artist @relation(fields: [artistId], references: [id]) + artistId Int @map("artist_id") + role ArtistRole @relation(fields: [artistRoleId], references: [id]) + artistRoleId Int @map("artist_role_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("creation") + @@schema("core") +} + + + +// Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.). +// In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese). +// That's why we need to have both SvsEngine and SvsEngineVersion. +model Performance { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + singer Singer @relation(fields: [singerId], references: [id]) + singerId Int @map("singer_id") + voicebank Voicebank? @relation(fields: [voicebankId], references: [id]) + voicebankId Int? @map("voicebank_id") + svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id]) + svsEngineId Int? @map("svs_engine_id") + svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id]) + svsEngineVersionId Int? @map("svs_engine_version_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("performance") + @@schema("core") +} + + + +model SongInAlbum { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + album Album @relation(fields: [albumId], references: [id]) + albumId Int @map("album_id") + trackNumber Int @map("track_number") + discNumber Int @default(1) @map("disc_number") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("song_in_album") + @@schema("core") +} + + + +model SongInSeries { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + series SongSeries @relation(fields: [seriesId], references: [id]) + seriesId Int @map("series_id") + order Int + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("song_in_series") + @@schema("core") +} + + + +model SongTag { + id Int @id @default(autoincrement()) + song Song @relation(fields: [songId], references: [id]) + songId Int @map("song_id") + tag Tag @relation(fields: [tagId], references: [id]) + tagId Int @map("tag_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("song_tag") + @@schema("core") +} + + + +model RolePermission { + id Int @id @default(autoincrement()) + role Role @relation(fields: [roleId], references: [id]) + roleId Int @map("role_id") + permission Permission @relation(fields: [permissionId], references: [id]) + permissionId Int @map("permission_id") + + @@map("role_permission") + @@schema("meta") +} + + + +# Relations + +This directory contains join tables for Many-to-Many relationships. + +To maintain a clean and modular Prisma schema, we separate core data models (in `/models`) from their association entities. +This structure helps prevent individual `.prisma` files from becoming too large and complex. + + + +/* +NOTE: +This is an empty placeholder file to ensure the Prisma Language Server functions +correctly, serving as a temporary workaround. + +See the link below for details: +https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600 + +This issue persists as of Prisma v7.3.0, which was the latest +version at the time of writing. +*/ + + + +generator client { + provider = "prisma-client" + output = "./generated" +} + +generator json { + provider = "prisma-json-types-generator" +} + +datasource db { + provider = "postgresql" + schemas = ["core", "meta", "platform", "auth"] +} + +generator zod { + provider = "prisma-zod" + output = "./zod" +} + +/* +# CVSA (Chinese Vocal Synthesis Archive) Database Schema + +## Overview +The CVSA is a structured encyclopedia and data platform dedicated to the Chinese Vocal Synthesis (SVS/VC) community. +It aims to bridge the gap between traditional wikis (like MediaWiki) and structured databases by providing: +- Automation: Fully automated song discovery, metadata extraction, and statistical tracking. +- Structured Data: Moving beyond flat text to rich, relational data for songs, artists, engines, and albums. +- Collaboration: A platform for contributors to provide descriptive content, translations, and corrections. + +## Schemas +The database is partitioned into several PostgreSQL schemas. + +### A. [core] - The Encyclopedia +This is the heart of the project. It stores universal SVS data that remains objective across platforms. +- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), +SVS Engines (Vocaloid, Synthesizer V), Voicebanks, Albums, and Tags. +- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata +(Simplified Chinese, Traditional Chinese, Japanese, English, etc.). +- Versioning: Supports tracking of specific SVS engine versions and voicebank iterations. + +### B. [platform] - The Service Layer +Contains data specific to the CVSA website as a functional service. +- Features: User-uploaded files, personal notes, etc. +- Scope: Data here is specific to this implementation and not necessarily shared with other archives. + +### C. [auth] - Identity & Access +Handles user accounts, session persistence, and secure authentication. +- RBAC: Connects users to roles defined in the `meta` schema. + +### D. [meta] - Governance & Auditing +Manages the "data about data." +- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) +in the `core` schema for accountability and rollbacks. +- Permissions: Granular action-based permissions assigned to roles. + +## 3. Technical Constraints & Design Patterns +- Soft Deletion: Most core entities implement a `deletedAt` timestamp for soft-deletion support. +- Modular Relations: Many-to-Many relations are extracted into separate files +within the `/relations` directory to keep entity definitions clean and prevent Prisma file bloat. + +## 4. Current Status & TODO +- Indexing: Performance indexes and unique constraints beyond primary keys are still under design. +- Constraints: Complex business logic constraints (e.g., preventing circular tag parents) are handled at the application layer. + +*/ + + + +declare global { + namespace PrismaJson { + type LocalizedField = { + [lang: string]: string; + }; + } +} + +// This file must be a module. +export {}; + + + +DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa + + + +prisma/generated +prisma/zod + + + +export * from "./prisma/generated/client"; +export * from "./prisma/generated/models"; +export * from "./prisma/generated/enums"; +export * from "./prisma/zod"; + + + +{ + "name": "@cvsa/db", + "type": "module", + "module": "index.ts", + "scripts": { + "gen": "bunx --bun prisma generate", + "migrate": "bunx --bun prisma migrate dev --create-only", + "migrate:dev": "bunx --bun prisma migrate dev", + "push": "bunx --bun prisma db push", + "db:deploy": "bunx --bun prisma migrate deploy", + "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", + "studio": "bunx --bun prisma studio", + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@cvsa/prisma-zod": "^3.3.0", + "@cvsa/typescript-config": "workspace:*", + "prisma": "^7.6.0", + "prisma-json-types-generator": "^4.1.1" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.6.0", + "@prisma/client": "^7.6.0", + "@types/pg": "^8.16.0", + "zod": "^4.3.6" + } +} + + + +// This file was generated by Prisma, and assumes you run Prisma commands using `bun --bun run prisma [command]`. +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); + + + +# @cvsa/db + +## Available Scripts + +- `gen` – Generate Prisma client and schema artifacts +- `deploy` – Deploy database changes +- `migrate` – Create a new migration file without applying it +- `migrate:dev` – Apply migrations to the development database +- `push` – Push schema changes directly to the database +- `test:setup` – Reset and apply migrations for test environment +- `studio` – Open Prisma Studio database GUI + + + +{ + "extends": "@cvsa/typescript-config/base.json", + "include": ["index.ts"] +} + + + diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 9fe3128..6a9bec0 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "@project-cvsa/typescript-config/base.json", + "extends": "@cvsa/typescript-config/base.json", "include": ["index.ts"] } diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 036a389..888f841 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,4 +1,4 @@ { - "name": "@project-cvsa/typescript-config", + "name": "@cvsa/typescript-config", "private": true } From 2deedd8dcd21e5677852f229789167988dc16855 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Wed, 1 Apr 2026 15:11:22 +0800 Subject: [PATCH 10/37] feat: support MeiliSearch, refactor architecture (#32) - Add support for MeiliSearch - Use a custom model2vec embedding model - Use Docker compose to simplify container preparation - Add DI container with singleton pattern - Refactor to use singleton service instances - Update repository interface and implementation with optional tx parameter --- .env.docker.example | 7 + .github/workflows/ci.yml | 43 + .gitignore | 7 +- .vscode/settings.json | 3 + apps/backend/.env.example | 2 + apps/backend/biome.json | 19 +- apps/backend/package.json | 5 +- apps/backend/src/common/env.ts | 13 - apps/backend/src/common/prisma.ts | 9 - apps/backend/src/index.ts | 2 + apps/backend/src/modules/auth/currentUser.ts | 56 +- apps/backend/src/modules/auth/login.ts | 2 +- apps/backend/src/modules/auth/signup.ts | 2 +- .../src/modules/catalog/song/create.ts | 4 +- .../src/modules/catalog/song/delete.ts | 4 +- .../src/modules/catalog/song/details.ts | 10 +- .../backend/src/modules/catalog/song/index.ts | 4 +- apps/backend/src/modules/catalog/song/list.ts | 20 - .../src/modules/catalog/song/search.ts | 21 + .../src/modules/catalog/song/update.ts | 50 +- apps/backend/src/startMessage.ts | 2 +- apps/backend/tests/auth.test.ts | 28 +- apps/backend/tests/song.test.ts | 96 +- biome.json | 7 +- bun.lock | 203 +++- docker-compose.yml | 29 + package.json | 12 +- packages/core/.env.example | 2 + packages/core/common/env.ts | 2 + packages/core/common/index.ts | 2 + packages/core/common/internal.ts | 1 + packages/core/common/prisma.ts | 43 +- packages/core/common/types/TxClient.ts | 6 + packages/core/common/types/index.ts | 3 + packages/core/common/types/repository.ts | 5 + packages/core/common/types/service.ts | 3 + .../core/common/utils/deepEqualUnordered.ts | 37 + packages/core/common/utils/index.ts | 1 + packages/core/modules/auth/betterAuth.ts | 10 +- packages/core/modules/auth/user/dto.ts | 14 +- .../core/modules/catalog/song/container.ts | 12 + packages/core/modules/catalog/song/dto.ts | 78 +- packages/core/modules/catalog/song/index.ts | 2 + .../catalog/song/repository.interface.ts | 26 +- .../core/modules/catalog/song/repository.ts | 164 ++- packages/core/modules/catalog/song/service.ts | 62 +- packages/core/modules/index.ts | 1 + packages/core/modules/internal.ts | 3 + packages/core/modules/ml/embedding.ts | 124 +++ packages/core/modules/ml/index.ts | 1 + packages/core/modules/search/catalog/index.ts | 1 + packages/core/modules/search/catalog/song.ts | 129 +++ packages/core/modules/search/config.ts | 50 + packages/core/modules/search/container.ts | 3 + packages/core/modules/search/index.ts | 2 + packages/core/modules/search/manager.ts | 183 ++++ .../core/modules/search/search.interface.ts | 14 + packages/core/package.json | 7 +- .../tests/integration/SongRepository.test.ts | 81 +- packages/core/tests/unit/SongService.test.ts | 83 +- packages/core/tests/utils.ts | 13 + packages/db/index.ts | 1 + packages/db/package.json | 4 +- packages/db/prisma/models/core/album.prisma | 1 + packages/db/prisma/models/core/artist.prisma | 1 + packages/db/prisma/models/core/lyrics.prisma | 2 +- packages/db/prisma/models/core/singer.prisma | 1 + packages/db/prisma/models/core/song.prisma | 5 +- .../db/prisma/models/core/songSeries.prisma | 1 + packages/db/prisma/schema.prisma | 7 +- packages/db/prisma/types.ts | 10 +- packages/db/prisma/zodSchema.ts | 3 + packages/db/schema.xml | 955 ------------------ turbo.json | 18 +- 74 files changed, 1333 insertions(+), 1504 deletions(-) create mode 100644 .env.docker.example create mode 100644 .github/workflows/ci.yml delete mode 100644 apps/backend/src/common/env.ts delete mode 100644 apps/backend/src/common/prisma.ts delete mode 100644 apps/backend/src/modules/catalog/song/list.ts create mode 100644 apps/backend/src/modules/catalog/song/search.ts create mode 100644 docker-compose.yml create mode 100644 packages/core/common/types/TxClient.ts create mode 100644 packages/core/common/types/index.ts create mode 100644 packages/core/common/types/repository.ts create mode 100644 packages/core/common/types/service.ts create mode 100644 packages/core/common/utils/deepEqualUnordered.ts create mode 100644 packages/core/modules/catalog/song/container.ts create mode 100644 packages/core/modules/internal.ts create mode 100644 packages/core/modules/ml/embedding.ts create mode 100644 packages/core/modules/ml/index.ts create mode 100644 packages/core/modules/search/catalog/index.ts create mode 100644 packages/core/modules/search/catalog/song.ts create mode 100644 packages/core/modules/search/config.ts create mode 100644 packages/core/modules/search/container.ts create mode 100644 packages/core/modules/search/index.ts create mode 100644 packages/core/modules/search/manager.ts create mode 100644 packages/core/modules/search/search.interface.ts create mode 100644 packages/core/tests/utils.ts create mode 100644 packages/db/prisma/zodSchema.ts delete mode 100644 packages/db/schema.xml diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..ed6bf72 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,7 @@ +# PostgreSQL +POSTGRES_USER=cvsa +POSTGRES_PASSWORD=password +POSTGRES_DB=cvsa + +# Meilisearch +MEILI_MASTER_KEY=0712041211241099 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7ee67a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + workflow_dispatch: + +jobs: + check: + name: Test and Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - name: Prepare .env files + run: | + cp .env.docker.example .env.docker + cp packages/db/.env.example packages/db/.env + cp packages/db/.env.example packages/db/.env.test + cp packages/core/.env.example packages/core/.env.test + cp apps/backend/.env.example apps/backend/.env.test + + - name: Start services with Docker Compose + run: docker compose up -d + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Setup environment + run: bun run setup:ci + + - name: Test + run: bun run test + + - name: Lint + run: bun run ci diff --git a/.gitignore b/.gitignore index 0e35deb..0361df4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env +.env.docker .env.development.local .env.test.local .env.test @@ -36,4 +37,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json data -.turbo \ No newline at end of file +.turbo + +# ML models + +model diff --git a/.vscode/settings.json b/.vscode/settings.json index 624bcae..d5440cf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,8 @@ }, "[prisma]": { "editor.defaultFormatter": "Prisma.prisma" + }, + "[github-actions-workflow]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/apps/backend/.env.example b/apps/backend/.env.example index ac56e81..5902192 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -1 +1,3 @@ DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa +MEILI_MASTER_KEY=0712041211241099 +MEILI_API_URL=http://127.0.0.1:7700 diff --git a/apps/backend/biome.json b/apps/backend/biome.json index 1126570..57f794e 100644 --- a/apps/backend/biome.json +++ b/apps/backend/biome.json @@ -1,6 +1,21 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "root": false, "extends": "//", - "plugins": ["./config/no-elysia-typebox.grit", "./config/no-try-in-handler.grit", "./config/no-zod-any.grit"] + "grit": { + "formatter": { + "enabled": false + }, + "assist": { + "enabled": false + }, + "linter": { + "enabled": false + } + }, + "plugins": [ + "./config/no-elysia-typebox.grit", + "./config/no-try-in-handler.grit", + "./config/no-zod-any.grit" + ] } diff --git a/apps/backend/package.json b/apps/backend/package.json index 3d6c806..59bafdb 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -17,11 +17,12 @@ }, "dependencies": { "@better-auth/prisma-adapter": "^1.5.4", + "@cvsa/core": "workspace:*", + "@cvsa/db": "workspace:*", "@elysiajs/eden": "^1.0.5", + "@elysiajs/openapi": "^1.4.14", "@prisma/adapter-pg": "^7.4.1", - "@cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", - "@cvsa/core": "workspace:*", "argon2": "^0.44.0", "better-auth": "^1.5.4", "chalk": "^5.6.2", diff --git a/apps/backend/src/common/env.ts b/apps/backend/src/common/env.ts deleted file mode 100644 index 823f926..0000000 --- a/apps/backend/src/common/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const env = createEnv({ - server: { - DATABASE_URL: z.url(), - NODE_ENV: z.enum(["development", "test", "production"]).optional(), - }, - - runtimeEnv: import.meta.env, - - emptyStringAsUndefined: true, -}); diff --git a/apps/backend/src/common/prisma.ts b/apps/backend/src/common/prisma.ts deleted file mode 100644 index cbfe2d5..0000000 --- a/apps/backend/src/common/prisma.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PrismaClient } from "@cvsa/db"; -import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "@common/env"; - -const adapter = new PrismaPg({ - connectionString: env.DATABASE_URL, -}); - -export const prisma = new PrismaClient({ adapter }); diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 1e35d73..8bca8b4 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -5,6 +5,7 @@ import pkg from "../package.json"; import { authHandler, songHandler } from "@modules/index"; import { AppError } from "@cvsa/core"; import { errorHandler } from "./errorHandler"; +import { openapi } from "@elysiajs/openapi"; const [host, port] = getBindingInfo(); logStartup(host, port); @@ -19,6 +20,7 @@ export const app = new Elysia({ AppError, }) .onError(errorHandler) + .use(openapi()) .use(authHandler) .use(songHandler) .use(onAfterHandler) diff --git a/apps/backend/src/modules/auth/currentUser.ts b/apps/backend/src/modules/auth/currentUser.ts index a549a18..ebb8832 100644 --- a/apps/backend/src/modules/auth/currentUser.ts +++ b/apps/backend/src/modules/auth/currentUser.ts @@ -1,28 +1,28 @@ -import { Elysia } from "elysia"; -import { - CurrentUserInfoSchema, - betterAuthToCurrentUserInfoDto, - ErrorResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@common/middlewares"; - -export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( - "/me", - async ({ status, session }) => { - const userInfo = betterAuthToCurrentUserInfoDto(session.user); - return status(200, userInfo); - }, - { - detail: { - summary: "Get current user", - description: "", - }, - response: { - 200: CurrentUserInfoSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 422: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); +import { Elysia } from "elysia"; +import { + CurrentUserInfoSchema, + betterAuthToCurrentUserInfoDto, + ErrorResponseSchema, +} from "@cvsa/core"; +import { authMiddleware } from "@common/middlewares"; + +export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( + "/me", + async ({ status, session }) => { + const userInfo = betterAuthToCurrentUserInfoDto(session.user); + return status(200, userInfo); + }, + { + detail: { + summary: "Get current user", + description: "", + }, + response: { + 200: CurrentUserInfoSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 422: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/modules/auth/login.ts b/apps/backend/src/modules/auth/login.ts index ddb6e63..e70f3d1 100644 --- a/apps/backend/src/modules/auth/login.ts +++ b/apps/backend/src/modules/auth/login.ts @@ -10,7 +10,7 @@ import { ErrorResponseSchema, betterAuthToLoginUserInfoDto, toLoginResponse, - toBetterAuthHeaders, + toBetterAuthHeaders, } from "@cvsa/core"; const DAY = 86400; diff --git a/apps/backend/src/modules/auth/signup.ts b/apps/backend/src/modules/auth/signup.ts index 5c204f0..119552a 100644 --- a/apps/backend/src/modules/auth/signup.ts +++ b/apps/backend/src/modules/auth/signup.ts @@ -8,7 +8,7 @@ import { SignupResponseSchema, toSignUpResponse, ErrorResponseSchema, - toBetterAuthHeaders + toBetterAuthHeaders, } from "@cvsa/core"; import { RateLimitError } from "@common/error"; import { AppError } from "@cvsa/core"; diff --git a/apps/backend/src/modules/catalog/song/create.ts b/apps/backend/src/modules/catalog/song/create.ts index b93530e..fa2df95 100644 --- a/apps/backend/src/modules/catalog/song/create.ts +++ b/apps/backend/src/modules/catalog/song/create.ts @@ -1,12 +1,12 @@ import { Elysia } from "elysia"; -import { SongService, CreateSongRequestSchema, ErrorResponseSchema } from "@cvsa/core"; +import { CreateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; import { SongSchema } from "@cvsa/db"; export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( "/song", async ({ body, status }) => { - const song = await new SongService().create(body); + const song = await songService.create(body); return status(201, song); }, { diff --git a/apps/backend/src/modules/catalog/song/delete.ts b/apps/backend/src/modules/catalog/song/delete.ts index 68d5e8e..3ff1244 100644 --- a/apps/backend/src/modules/catalog/song/delete.ts +++ b/apps/backend/src/modules/catalog/song/delete.ts @@ -1,6 +1,6 @@ import { Elysia } from "elysia"; import { z } from "zod"; -import { SongService, ErrorResponseSchema } from "@cvsa/core"; +import { ErrorResponseSchema, songService } from "@cvsa/core"; import { AppError } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; @@ -13,7 +13,7 @@ export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) if (Number.isNaN(id)) { throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); } - await new SongService().delete(id); + await songService.delete(id); set.status = 204; return null; }, diff --git a/apps/backend/src/modules/catalog/song/details.ts b/apps/backend/src/modules/catalog/song/details.ts index d8c9e5c..5143c5e 100644 --- a/apps/backend/src/modules/catalog/song/details.ts +++ b/apps/backend/src/modules/catalog/song/details.ts @@ -1,12 +1,12 @@ import { Elysia } from "elysia"; -import { SongService, ErrorResponseSchema, SongDetailsResponseSchema } from "@cvsa/core"; +import { ErrorResponseSchema, SongDetailsResponseSchema, songService } from "@cvsa/core"; import z from "zod"; export const songDetailsHandler = new Elysia().get( "/song/:id/details", - async ({ params }) => { - const song = await new SongService().getDetails(params.id); - return song; + async ({ params, status }) => { + const song = await songService.getDetails(params.id); + return status(200, song); }, { detail: { @@ -19,7 +19,7 @@ export const songDetailsHandler = new Elysia().get( 404: ErrorResponseSchema, }, params: z.object({ - id: z.int().positive(), + id: z.coerce.number().int().positive(), }), } ); diff --git a/apps/backend/src/modules/catalog/song/index.ts b/apps/backend/src/modules/catalog/song/index.ts index 78c5432..adfa5af 100644 --- a/apps/backend/src/modules/catalog/song/index.ts +++ b/apps/backend/src/modules/catalog/song/index.ts @@ -2,12 +2,12 @@ import { Elysia } from "elysia"; import { songCreateHandler } from "./create"; import { songUpdateHandler } from "./update"; import { songDeleteHandler } from "./delete"; -import { songListHandler } from "./list"; import { songDetailsHandler } from "./details"; +import { songSearchHandler } from "./search"; export const songHandler = new Elysia({ name: "songHandler" }) .use(songDetailsHandler) .use(songCreateHandler) .use(songUpdateHandler) .use(songDeleteHandler) - .use(songListHandler); + .use(songSearchHandler); diff --git a/apps/backend/src/modules/catalog/song/list.ts b/apps/backend/src/modules/catalog/song/list.ts deleted file mode 100644 index 84b6d87..0000000 --- a/apps/backend/src/modules/catalog/song/list.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Elysia } from "elysia"; -import { SongService, ListSongsQuerySchema, ListSongsResponseSchema } from "@cvsa/core"; - -export const songListHandler = new Elysia({ name: "songListHandler" }).get( - "/song", - async ({ query }) => { - const result = await new SongService().list(query); - return result; - }, - { - query: ListSongsQuerySchema, - detail: { - summary: "List Songs", - description: "List songs with pagination (no authentication required)", - }, - response: { - 200: ListSongsResponseSchema, - }, - } -); diff --git a/apps/backend/src/modules/catalog/song/search.ts b/apps/backend/src/modules/catalog/song/search.ts new file mode 100644 index 0000000..8f0f8a8 --- /dev/null +++ b/apps/backend/src/modules/catalog/song/search.ts @@ -0,0 +1,21 @@ +import { Elysia } from "elysia"; +import { songSearchService } from "@cvsa/core"; +import z from "zod"; + +// TODO: add corresponding DTO and response schema +export const songSearchHandler = new Elysia().get( + "/songs", + async ({ query, status }) => { + const song = await songSearchService.search(query.q || ""); + return status(200, song); + }, + { + detail: { + summary: "Search for Songs", + description: "", + }, + query: z.object({ + q: z.string().optional(), + }), + } +); diff --git a/apps/backend/src/modules/catalog/song/update.ts b/apps/backend/src/modules/catalog/song/update.ts index d76571c..643130f 100644 --- a/apps/backend/src/modules/catalog/song/update.ts +++ b/apps/backend/src/modules/catalog/song/update.ts @@ -1,30 +1,32 @@ import { Elysia } from "elysia"; -import { SongService, UpdateSongRequestSchema, ErrorResponseSchema } from "@cvsa/core"; +import { UpdateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; import { AppError } from "@cvsa/core"; import { authMiddleware } from "@common/middlewares/auth"; import { SongSchema } from "@cvsa/db"; -export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }).use(authMiddleware).put( - "/song/:id", - async ({ params, body }) => { - const id = Number(params.id); - if (Number.isNaN(id)) { - throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); - } - const song = await new SongService().update(id, body); - return song; - }, - { - body: UpdateSongRequestSchema, - detail: { - summary: "Update Song", - description: "Update an existing song (authentication required)", - }, - response: { - 200: SongSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, +export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }) + .use(authMiddleware) + .patch( + "/song/:id", + async ({ params, body, status }) => { + const id = Number(params.id); + if (Number.isNaN(id)) { + throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); + } + const song = await songService.update(id, body); + return status(200, song); }, - } -); + { + body: UpdateSongRequestSchema, + detail: { + summary: "Update Song", + description: "Update an existing song (authentication required)", + }, + response: { + 200: SongSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/startMessage.ts b/apps/backend/src/startMessage.ts index ea4443d..b57fb83 100644 --- a/apps/backend/src/startMessage.ts +++ b/apps/backend/src/startMessage.ts @@ -1,4 +1,4 @@ -import { env } from "@common/env"; +import { env } from "@cvsa/core"; import chalk from "chalk"; import os from "node:os"; diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index ca0c693..ba562f0 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@common/prisma"; +import { prisma } from "@cvsa/core"; const api = treaty(app); @@ -293,27 +293,21 @@ describe("Logout E2E Tests - DELETE /v2/session", () => { const { data: signUpData } = await api.v2.user.post(signupPayload); - const { status } = await api.v2.session.delete( - null, - { - headers: { - authorization: `Bearer ${signUpData?.data.token}`, - }, - } - ); + const { status } = await api.v2.session.delete(null, { + headers: { + authorization: `Bearer ${signUpData?.data.token}`, + }, + }); expect(status).toBe(204); }); test("should return 401 on invalid/expired token", async () => { - const { error, status } = await api.v2.session.delete( - null, - { - headers: { - authorization: "Bearer invalid.token.format", - }, - } - ); + const { error, status } = await api.v2.session.delete(null, { + headers: { + authorization: "Bearer invalid.token.format", + }, + }); expect(status).toBe(401); expect(error?.value).toMatchObject({ diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts index c982e90..f3994a4 100644 --- a/apps/backend/tests/song.test.ts +++ b/apps/backend/tests/song.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@common/prisma"; +import { prisma } from "@cvsa/core"; const api = treaty(app); @@ -66,7 +66,7 @@ describe("Song E2E Tests", () => { }); }); - describe("PUT /v2/song/:id - Update Song", () => { + describe("PATCH /v2/song/:id - Update Song", () => { test("should update a song with authentication", async () => { const token = await getAuthToken(); @@ -76,7 +76,7 @@ describe("Song E2E Tests", () => { }, }); - const { data, status } = await api.v2.song({ id: song.id }).put( + const { data, status } = await api.v2.song({ id: song.id }).patch( { name: "Updated Name" }, { headers: { @@ -96,7 +96,7 @@ describe("Song E2E Tests", () => { }, }); - const { status } = await api.v2.song({ id: song.id }).put({ + const { status } = await api.v2.song({ id: song.id }).patch({ name: "Updated Name", }); @@ -106,7 +106,7 @@ describe("Song E2E Tests", () => { test("should return 404 for non-existent song", async () => { const token = await getAuthToken(); - const { error, status } = await api.v2.song({ id: 999999 }).put( + const { error, status } = await api.v2.song({ id: 999999 }).patch( { name: "Updated Name" }, { headers: { @@ -176,90 +176,4 @@ describe("Song E2E Tests", () => { expect(status).toBe(404); }); }); - - describe("GET /v2/song - List Songs", () => { - test("should list songs without authentication", async () => { - await prisma.song.create({ - data: { name: "Song 1" }, - }); - await prisma.song.create({ - data: { name: "Song 2" }, - }); - - const { data, status } = await api.v2.song.get(); - - expect(status).toBe(200); - expect(data?.songs).toHaveLength(2); - expect(data?.total).toBe(2); - }); - - test("should support pagination", async () => { - for (let i = 1; i <= 5; i++) { - await prisma.song.create({ - data: { name: `Song ${i}` }, - }); - } - - const { data, status } = await api.v2.song.get({ - query: { offset: 0, limit: 2 }, - }); - - expect(status).toBe(200); - expect(data?.songs).toHaveLength(2); - expect(data?.total).toBe(5); - }); - - test("should filter by search term", async () => { - await prisma.song.create({ - data: { name: "Unique Song Name" }, - }); - await prisma.song.create({ - data: { name: "Another Song" }, - }); - - const { data, status } = await api.v2.song.get({ - query: { search: "Unique" }, - }); - - expect(status).toBe(200); - expect(data?.songs).toHaveLength(1); - expect(data?.songs[0]?.name).toBe("Unique Song Name"); - }); - - test("should filter by type", async () => { - await prisma.song.create({ - data: { name: "Original Song", type: "ORIGINAL" }, - }); - await prisma.song.create({ - data: { name: "Cover Song", type: "COVER" }, - }); - - const { data, status } = await api.v2.song.get({ - query: { type: "ORIGINAL" }, - }); - - expect(status).toBe(200); - expect(data?.songs).toHaveLength(1); - expect(data?.songs[0]?.type).toBe("ORIGINAL"); - }); - - test("should exclude soft-deleted songs", async () => { - await prisma.song.create({ - data: { name: "Active Song" }, - }); - const deletedSong = await prisma.song.create({ - data: { name: "Deleted Song" }, - }); - await prisma.song.update({ - where: { id: deletedSong.id }, - data: { deletedAt: new Date() }, - }); - - const { data, status } = await api.v2.song.get(); - - expect(status).toBe(200); - expect(data?.songs).toHaveLength(1); - expect(data?.songs[0]?.name).toBe("Active Song"); - }); - }); }); diff --git a/biome.json b/biome.json index 74ee388..4be8145 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "assist": { "actions": { "source": { @@ -21,7 +21,8 @@ "suspicious": { "noExplicitAny": "error", "noDeprecatedImports": "error" - } + }, + "nursery": {} } }, "javascript": { @@ -50,6 +51,6 @@ "useIgnoreFile": true }, "files": { - "includes": ["**/*", "!.zed/*", "!packages/typescript-config/base.json"] + "includes": ["**/*", "!.zed/*", "!packages/typescript-config/base.json", "!**/*.grit"] } } diff --git a/bun.lock b/bun.lock index 1422d70..78b40d6 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "cvsa", "devDependencies": { - "@biomejs/biome": "^2.4.8", + "@biomejs/biome": "^2.4.10", "@types/bun": "^1.3.10", "turbo": "^2.8.18", "typescript": "^6.0.2", @@ -19,6 +19,7 @@ "@cvsa/core": "workspace:*", "@cvsa/db": "workspace:*", "@elysiajs/eden": "^1.0.5", + "@elysiajs/openapi": "^1.4.14", "@prisma/adapter-pg": "^7.4.1", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", @@ -41,9 +42,13 @@ "dependencies": { "@better-auth/prisma-adapter": "^1.5.5", "@cvsa/db": "*", + "@huggingface/hub": "^2.11.0", + "@huggingface/transformers": "^4.0.0", "@t3-oss/env-core": "^0.13.10", "better-auth": "^1.5.5", + "meilisearch": "^0.56.0", "nanoid": "^5.1.6", + "onnxruntime-node": "^1.24.3", "remeda": "^2.33.6", "zod": "^4.3.6", }, @@ -62,7 +67,7 @@ "zod": "^4.3.6", }, "devDependencies": { - "@cvsa/prisma-zod": "^3.3.0", + "@cvsa/prisma-zod": "^3.6.0", "@cvsa/typescript-config": "workspace:*", "prisma": "^7.6.0", "prisma-json-types-generator": "^4.1.1", @@ -93,23 +98,23 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], @@ -123,7 +128,7 @@ "@cvsa/db": ["@cvsa/db@workspace:packages/db"], - "@cvsa/prisma-zod": ["@cvsa/prisma-zod@3.3.0", "", { "dependencies": { "@prisma/generator-helper": "^7.5.0" }, "peerDependencies": { "@prisma/client": ">= 7.0.0", "prisma": ">= 7.0.0", "zod": "^4.3.6" }, "bin": { "prisma-zod": "dist/index.js" } }, "sha512-FaLH1aNBAbS5Vd96G9TCPR/piacRf2zWNL8UcheTHAvcaGNCLOCEOAthpelcmI8YKU2FDXxpmZZSqSz3hj8ZbA=="], + "@cvsa/prisma-zod": ["@cvsa/prisma-zod@3.6.0", "", { "dependencies": { "@prisma/generator-helper": "^7.5.0" }, "peerDependencies": { "@prisma/client": ">= 7.0.0", "prisma": ">= 7.0.0", "zod": ">= 4.0.0" }, "bin": { "prisma-zod": "dist/index.js" } }, "sha512-5/Vh93EqlaXwwJk9cudIclmZG8fX4XsQ2LiGSSN5JcszOzm/MPXuyuGJU9M70X1P+KAB8Vza8RsvXdXmTTJbSw=="], "@cvsa/typescript-config": ["@cvsa/typescript-config@workspace:packages/typescript-config"], @@ -135,10 +140,74 @@ "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], + "@elysiajs/openapi": ["@elysiajs/openapi@1.4.14", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-kWmJWdvP8/LwHwAJXSpz6xFfYUoyUyEPRimEYABuDU1rOnS27Da1u9T2jyU7frOopxKWV/wDfDxMP8z2xdCPJw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + "@huggingface/hub": ["@huggingface/hub@2.11.0", "", { "dependencies": { "@huggingface/tasks": "^0.19.90" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ=="], + + "@huggingface/jinja": ["@huggingface/jinja@0.5.6", "", {}, "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA=="], + + "@huggingface/tasks": ["@huggingface/tasks@0.19.90", "", {}, "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA=="], + + "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], + + "@huggingface/transformers": ["@huggingface/transformers@4.0.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", "sharp": "^0.34.5" } }, "sha512-7shdBRvtjIDP7UUhdi0b4CMRCD+QXvQIsEsxECWS9VEDEsxSQF9Ci+51mwy2OWI8vbsoic8e4dbR4eI8aZWxaw=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], @@ -183,6 +252,26 @@ "@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -209,17 +298,17 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-d1zTcIf6VWT7cdfjhi0X36C2PRsUi2HdEwYzVgkLHmuuYtL+1Y1Zu3JdlouoB/NjG2vX3q4NnKLMNhDOEweoIg=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AwJ4mA++Kpem33Lcov093hS1LrgqbKxqq5FCReoqsA8ayEG6eAJAo8ItDd9qQTdBiXxZH8GHCspLAMIe1t3Xyw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HT9SjKkjEw9uvlgly/qwCGEm4wOXOwQPSPS+wkg+/O1Qan3F1uU/0PFYzxl3m4lfuV3CP9wr2Dq5dPrUX+B9Ag=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+4s5GZs3kjxc1KMhLBhoQy4UBkXjOhgidA9ipNllkA4JLivSqUCuOgU1Xbyp6vzYrsqHJ9vvwo/2mXgEtD6ZHg=="], - "@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ZO7GCyQd5HV564XWHc9KysjanFfM3DmnWquyEByu+hQMq42g9OMU/fYOCfHS6Xj2aXkIg2FHJeRV+iAck2YrbQ=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-BjX2fdz38mBb/H94JXrD5cJ+mEq8NmsCbYdC42JzQebJ0X8EdNgyFoEhOydPGViOmaRmhhdZnPZKKn6wahSpcA=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], @@ -233,8 +322,12 @@ "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], @@ -245,6 +338,8 @@ "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -259,6 +354,8 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -275,12 +372,20 @@ "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], @@ -291,10 +396,20 @@ "elysia-rate-limit": ["elysia-rate-limit@4.5.1", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-36u0BBMWV8Ae5f4q8k19Yuq4UusjbFTstZ7Ej1RU6m/Xc94RZHfai8kn5lOsC+LEduZJr4JdQlZu+jhuSxQfyw=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -309,6 +424,8 @@ "file-type": ["file-type@21.3.3", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg=="], + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -317,12 +434,22 @@ "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], + "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -331,6 +458,8 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -341,12 +470,18 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "kysely": ["kysely@0.28.14", "", {}, "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + + "meilisearch": ["meilisearch@0.56.0", "", {}, "sha512-kBXba8DcSrLWHYqopCm2JL90oBy97VdIfHkP1ii7/eHufeEEEk9Zu5Vv/IFQpNzmhEpWszVFVlGvZoI6QJeGzA=="], + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], @@ -373,8 +508,16 @@ "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="], + + "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], + + "onnxruntime-web": ["onnxruntime-web@1.25.0-dev.20260327-722743c0e2", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-8PXdZy4Ekhg10CLg+cFFt39b4tFDGMRJB6lGjnQL6eA+2boUQYDymZ0gtxiS+H6oIWoCjQp/ziyirvFbaFKfiw=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -403,6 +546,8 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], @@ -419,6 +564,8 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], @@ -437,6 +584,8 @@ "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -445,10 +594,16 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -461,10 +616,16 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], @@ -477,7 +638,9 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="], + "turbo": ["turbo@2.9.1", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.1", "@turbo/darwin-arm64": "2.9.1", "@turbo/linux-64": "2.9.1", "@turbo/linux-arm64": "2.9.1", "@turbo/windows-64": "2.9.1", "@turbo/windows-arm64": "2.9.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-TO9du8MwLTAKoXcGezekh9cPJabJUb0+8KxtpMR6kXdRASrmJ8qXf2GkVbCREgzbMQakzfNcux9cZtxheDY4RQ=="], + + "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], @@ -513,6 +676,8 @@ "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2ec7a0b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + db: + image: postgres:18-alpine + container_name: cvsa-db + restart: always + ports: + - "127.0.0.1:5432:5432" + environment: + - PGDATA=/var/lib/postgresql/18/docker + env_file: + - .env.docker + volumes: + - data:/var/lib/postgresql + command: postgres + + meili: + image: getmeili/meilisearch:v1.40 + container_name: cvsa-meili + restart: always + ports: + - "127.0.0.1:7700:7700" + env_file: + - .env.docker + volumes: + - meili:/meili_data + +volumes: + data: + meili: diff --git a/package.json b/package.json index 8cc004d..5fbbf28 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,19 @@ "dev": "bunx --bun turbo run dev", "build": "bunx --bun turbo run build", "lint": "bunx --bun turbo run lint", - "lint:fix": "biome check --write --unsafe .", - "format": "biome format --write .", + "lint:fix": "bunx --bun biome check --write --unsafe .", + "format": "bunx --bun biome format --write .", + "ci": "bunx --bun biome ci .", "test": "bunx --bun turbo run test", + "test:backend": "bunx --bun turbo run test --filter=@cvsa/backend", + "test:core": "bunx --bun turbo run test --filter=@cvsa/core", "test:coverage": "bunx --bun turbo run test:coverage", "typecheck": "bunx --bun turbo typecheck", - "db:deploy": "bunx --bun turbo db:deploy" + "setup": "bunx --bun turbo setup", + "setup:ci": "bunx --bun turbo setup:ci" }, "devDependencies": { - "@biomejs/biome": "^2.4.8", + "@biomejs/biome": "^2.4.10", "@types/bun": "^1.3.10", "turbo": "^2.8.18", "typescript": "^6.0.2" diff --git a/packages/core/.env.example b/packages/core/.env.example index ac56e81..5902192 100644 --- a/packages/core/.env.example +++ b/packages/core/.env.example @@ -1 +1,3 @@ DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa +MEILI_MASTER_KEY=0712041211241099 +MEILI_API_URL=http://127.0.0.1:7700 diff --git a/packages/core/common/env.ts b/packages/core/common/env.ts index 823f926..e53d903 100644 --- a/packages/core/common/env.ts +++ b/packages/core/common/env.ts @@ -5,6 +5,8 @@ export const env = createEnv({ server: { DATABASE_URL: z.url(), NODE_ENV: z.enum(["development", "test", "production"]).optional(), + MEILI_MASTER_KEY: z.string().optional().default(""), + MEILI_API_URL: z.url().optional().default("http://127.0.0.1:7700"), }, runtimeEnv: import.meta.env, diff --git a/packages/core/common/index.ts b/packages/core/common/index.ts index a3f7d7e..952334b 100644 --- a/packages/core/common/index.ts +++ b/packages/core/common/index.ts @@ -1,2 +1,4 @@ export * from "./schema"; export * from "./error"; +export * from "./prisma"; +export * from "./env"; diff --git a/packages/core/common/internal.ts b/packages/core/common/internal.ts index 159d157..a05624b 100644 --- a/packages/core/common/internal.ts +++ b/packages/core/common/internal.ts @@ -7,3 +7,4 @@ export * from "./env"; export * from "./utils"; export * from "./prisma"; +export * from "./types"; diff --git a/packages/core/common/prisma.ts b/packages/core/common/prisma.ts index d452df8..0b99d5e 100644 --- a/packages/core/common/prisma.ts +++ b/packages/core/common/prisma.ts @@ -1,9 +1,50 @@ import { PrismaClient } from "@cvsa/db"; import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "@cvsa/core/common"; +import { env } from "./env"; const adapter = new PrismaPg({ connectionString: env.DATABASE_URL, }); +/** + * Convert an type to a JSON-serializable version. + * It will convert `bigint` to `number`, + * and `Date` to `string` + */ +export type Serialized = T extends Date + ? string + : T extends bigint + ? number + : T extends Array + ? Array> + : T extends object + ? { [K in keyof T]: Serialized } + : T; + +export function transformPrismaResult(obj: T): Serialized { + if (obj instanceof Date) { + return obj.toISOString() as Serialized; + } + + if (typeof obj === "bigint") { + return Number(obj) as Serialized; + } + + if (Array.isArray(obj)) { + return obj.map((item) => transformPrismaResult(item)) as Serialized; + } + + if (typeof obj === "object" && obj !== null) { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + result[key] = transformPrismaResult(value); + } + + return result as Serialized; + } + + return obj as Serialized; +} + export const prisma = new PrismaClient({ adapter }); diff --git a/packages/core/common/types/TxClient.ts b/packages/core/common/types/TxClient.ts new file mode 100644 index 0000000..71a0974 --- /dev/null +++ b/packages/core/common/types/TxClient.ts @@ -0,0 +1,6 @@ +import type { PrismaClient } from "@cvsa/db"; + +export type TxClient = Omit< + PrismaClient, + "$transaction" | "$connect" | "$disconnect" | "$on" | "$use" +>; diff --git a/packages/core/common/types/index.ts b/packages/core/common/types/index.ts new file mode 100644 index 0000000..a3910f4 --- /dev/null +++ b/packages/core/common/types/index.ts @@ -0,0 +1,3 @@ +export * from "./TxClient"; +export * from "./repository"; +export * from "./service"; diff --git a/packages/core/common/types/repository.ts b/packages/core/common/types/repository.ts new file mode 100644 index 0000000..dd2f09b --- /dev/null +++ b/packages/core/common/types/repository.ts @@ -0,0 +1,5 @@ +import type { TxClient } from "./TxClient"; + +export interface IDetailsRepository { + getDetailsById(id: number | string, tx?: TxClient): Promise; +} diff --git a/packages/core/common/types/service.ts b/packages/core/common/types/service.ts new file mode 100644 index 0000000..772a6a5 --- /dev/null +++ b/packages/core/common/types/service.ts @@ -0,0 +1,3 @@ +export interface IDetailsService { + getDetails(id: number | string): Promise; +} diff --git a/packages/core/common/utils/deepEqualUnordered.ts b/packages/core/common/utils/deepEqualUnordered.ts new file mode 100644 index 0000000..3a292e1 --- /dev/null +++ b/packages/core/common/utils/deepEqualUnordered.ts @@ -0,0 +1,37 @@ +type PlainObject = Record; + +const isObject = (obj: unknown): obj is PlainObject => { + return obj !== null && typeof obj === "object" && !Array.isArray(obj); +}; + +export const deepEqualUnordered = (a: unknown, b: unknown): boolean => { + if (a === b) return true; + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + + const bCopy = [...b]; + for (const itemA of a) { + const indexB = bCopy.findIndex((itemB) => deepEqualUnordered(itemA, itemB)); + if (indexB === -1) return false; + bCopy.splice(indexB, 1); + } + return true; + } + + if (isObject(a) && isObject(b)) { + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + // biome-ignore lint/suspicious/noPrototypeBuiltins: utility function + if (!Object.prototype.hasOwnProperty.call(b, key)) return false; + if (!deepEqualUnordered(a[key], b[key])) return false; + } + return true; + } + + return false; +}; diff --git a/packages/core/common/utils/index.ts b/packages/core/common/utils/index.ts index 75f9eaa..a681d76 100644 --- a/packages/core/common/utils/index.ts +++ b/packages/core/common/utils/index.ts @@ -1 +1,2 @@ export * from "./randomId"; +export * from "./deepEqualUnordered"; diff --git a/packages/core/modules/auth/betterAuth.ts b/packages/core/modules/auth/betterAuth.ts index d5ff4ec..3b7fddb 100644 --- a/packages/core/modules/auth/betterAuth.ts +++ b/packages/core/modules/auth/betterAuth.ts @@ -43,7 +43,15 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, - plugins: [username(), bearer()], + plugins: [ + username({ + minUsernameLength: 1, + maxUsernameLength: 100, + // We don't have any limitations of username + usernameValidator: () => true, + }), + bearer(), + ], }); export { APIError as BetterAuthAPIError }; diff --git a/packages/core/modules/auth/user/dto.ts b/packages/core/modules/auth/user/dto.ts index 06bffaa..553cc2d 100644 --- a/packages/core/modules/auth/user/dto.ts +++ b/packages/core/modules/auth/user/dto.ts @@ -11,9 +11,9 @@ type BetterAuthSignupRequestBody = Exclude< >["body"]; export const SignupRequestSchema = z.object({ - username: z.string().min(1), + username: z.string().min(1).max(100), password: z.string().min(8), - displayName: z.string().max(100).optional().nullable(), + displayName: z.string().min(1).max(100).optional().nullable(), email: z.email().optional().nullable(), }); @@ -30,7 +30,7 @@ export const CurrentUserInfoSchema = z.object({ username: z.string(), displayName: z.string().optional().nullable(), email: z.email().optional().nullable(), - createdAt: z.date().optional().nullable(), + createdAt: z.iso.datetime().nullish(), image: z.string().optional().nullable(), }); @@ -44,10 +44,12 @@ export type SignupUserInfoDto = z.infer; export type CurrentUserInfoDto = z.infer; export type SignupResponseDto = z.infer; -export function toBetterAuthHeaders(headers: Record): [string, string][] { +export function toBetterAuthHeaders( + headers: Record +): [string, string][] { return Object.entries(headers).filter( (entry): entry is [string, string] => entry[1] !== undefined - ) + ); } export function signupRequestToBetterAuth(body: SignupRequestDto): BetterAuthSignupRequestBody { @@ -80,7 +82,7 @@ export function betterAuthToCurrentUserInfoDto(user: BetterAuthUser): CurrentUse username: user.username ?? "", displayName: user.name, email: user.email, - createdAt: user.createdAt ?? null, + createdAt: user.createdAt.toISOString() ?? null, image: user.image ?? null, }; } diff --git a/packages/core/modules/catalog/song/container.ts b/packages/core/modules/catalog/song/container.ts new file mode 100644 index 0000000..cfaf523 --- /dev/null +++ b/packages/core/modules/catalog/song/container.ts @@ -0,0 +1,12 @@ +import { prisma } from "@cvsa/core/common"; +import { SongRepository } from "./repository"; +import { SongService } from "./service"; +import { SongSearchService, searchManager, embeddingManager } from "@cvsa/core/modules"; + +export const songRepository = new SongRepository(prisma); +export const songSearchService = new SongSearchService( + songRepository, + searchManager, + embeddingManager +); +export const songService = new SongService(songRepository, songSearchService); diff --git a/packages/core/modules/catalog/song/dto.ts b/packages/core/modules/catalog/song/dto.ts index ebf1f2e..a27f3e1 100644 --- a/packages/core/modules/catalog/song/dto.ts +++ b/packages/core/modules/catalog/song/dto.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { SongSchema, SingerSchema, ArtistSchema, ArtistRoleSchema, SongTypeSchema } from "@cvsa/db"; +import type { Serialized } from "@cvsa/core/common"; export type SongId = number; @@ -10,59 +11,48 @@ const CreateCreationSchema = z.object({ const CreatePerformanceSchema = z.object({ singerId: z.int().positive(), - voicebankId: z.int().positive().nullable().optional(), - svsEngineId: z.int().positive().nullable().optional(), - svsEngineVersionId: z.int().positive().nullable().optional(), + voicebankId: z.int().positive().nullish(), + svsEngineId: z.int().positive().nullish(), + svsEngineVersionId: z.int().positive().nullish(), }); export const CreateSongRequestSchema = z.object({ - type: SongTypeSchema.nullable().optional(), - name: z.string().nullable().optional(), - duration: z.number().nullable().optional(), - description: z.string().nullable().optional(), - coverUrl: z.url().nullable().optional(), - publishedAt: z.date().nullable().optional(), + type: SongTypeSchema.nullish(), + name: z.string().nullish(), + duration: z.number().nullish(), + description: z.string().nullish(), + coverUrl: z.url().nullish(), + publishedAt: z.iso.datetime().nullish(), performances: z.array(CreatePerformanceSchema).optional(), creations: z.array(CreateCreationSchema).optional(), }); export const UpdateSongRequestSchema = z.object({ - name: z.string().nullable().optional(), - type: SongTypeSchema.nullable().optional(), - duration: z.int().nullable().optional(), - description: z.string().nullable().optional(), - coverUrl: z.url().nullable().optional(), - publishedAt: z.date().nullable().optional() -}); - -export const ListSongsQuerySchema = z.object({ - search: z.string().optional(), - type: SongTypeSchema.optional(), - offset: z.coerce.number().int().min(0).optional(), - limit: z.coerce.number().int().min(1).max(100).optional(), -}); - -export const ListSongsResponseSchema = z.object({ - songs: z.array( - z.object({ - id: z.number(), - type: SongTypeSchema.nullable(), - name: z.string().nullable(), - duration: z.number().nullable(), - coverUrl: z.url().nullable(), - publishedAt: z.date().nullable(), - }) - ), - total: z.number(), - offset: z.number(), - limit: z.number(), + name: z.string().nullish(), + type: SongTypeSchema.nullish(), + duration: z.int().nullish(), + description: z.string().nullish(), + coverUrl: z.url().nullish(), + publishedAt: z.iso.datetime().nullish(), }); export const SongDetailsResponseSchema = z.intersection( SongSchema, z.object({ - singers: SingerSchema.array(), - artists: z.intersection(ArtistSchema, z.object({ role: ArtistRoleSchema })).array(), + singers: z + .intersection( + SingerSchema.omit({ deletedAt: true }), + z.object({ + engine: z.string().nullable(), + }) + ) + .array(), + artists: z + .intersection( + ArtistSchema.omit({ deletedAt: true }), + z.object({ role: ArtistRoleSchema }) + ) + .array(), lyrics: z.array( z.object({ plainText: z.string().nullable(), @@ -73,8 +63,6 @@ export const SongDetailsResponseSchema = z.intersection( }) ); -export type CreateSongRequestDto = z.infer; -export type UpdateSongRequestDto = z.infer; -export type ListSongsResponseDto = z.infer; -export type ListSongsQueryDto = z.infer; -export type SongDetailsResponseDto = z.infer; +export type CreateSongRequestDto = Serialized>; +export type UpdateSongRequestDto = Serialized>; +export type SongDetailsResponseDto = Serialized>; diff --git a/packages/core/modules/catalog/song/index.ts b/packages/core/modules/catalog/song/index.ts index d0cdb18..132cf0c 100644 --- a/packages/core/modules/catalog/song/index.ts +++ b/packages/core/modules/catalog/song/index.ts @@ -1,3 +1,5 @@ export * from "./dto"; export * from "./repository"; export * from "./service"; +export * from "./repository.interface"; +export * from "./container"; diff --git a/packages/core/modules/catalog/song/repository.interface.ts b/packages/core/modules/catalog/song/repository.interface.ts index 0d36469..22c9cf2 100644 --- a/packages/core/modules/catalog/song/repository.interface.ts +++ b/packages/core/modules/catalog/song/repository.interface.ts @@ -1,12 +1,20 @@ import type { Song } from "@cvsa/db"; -import type { SongDetailsResponseDto } from "./dto"; -import type { CreateSongRequestDto, ListSongsQueryDto, SongId, UpdateSongRequestDto } from "./dto"; +import type { TxClient, IDetailsRepository, Serialized } from "@cvsa/core/common"; +import type { + CreateSongRequestDto, + SongId, + UpdateSongRequestDto, + SongDetailsResponseDto, +} from "./dto"; -export interface ISongRepository { - getById(id: SongId): Promise; - getDetailsById(id: SongId): Promise; - list(query?: ListSongsQueryDto): Promise<{ songs: Song[]; total: number }>; - create(input: CreateSongRequestDto): Promise; - update(id: SongId, input: UpdateSongRequestDto): Promise; - softDelete(id: SongId): Promise; +export abstract class ISongRepository implements IDetailsRepository { + abstract getById(id: SongId, tx?: TxClient): Promise | null>; + abstract getDetailsById(id: SongId, tx?: TxClient): Promise; + abstract create(input: CreateSongRequestDto, tx?: TxClient): Promise>; + abstract update( + id: SongId, + input: UpdateSongRequestDto, + tx?: TxClient + ): Promise>; + abstract softDelete(id: SongId, tx?: TxClient): Promise; } diff --git a/packages/core/modules/catalog/song/repository.ts b/packages/core/modules/catalog/song/repository.ts index 061717a..7d26cc3 100644 --- a/packages/core/modules/catalog/song/repository.ts +++ b/packages/core/modules/catalog/song/repository.ts @@ -1,37 +1,62 @@ -import type { Prisma, PrismaClient } from "@cvsa/db"; -import type { SongDetailsResponseDto } from "./dto"; -import type { CreateSongRequestDto, ListSongsQueryDto, SongId, UpdateSongRequestDto } from "./dto"; +import type { PrismaClient } from "@cvsa/db"; +import type { + CreateSongRequestDto, + SongId, + UpdateSongRequestDto, + SongDetailsResponseDto, +} from "./dto"; import type { ISongRepository } from "./repository.interface"; +import { transformPrismaResult, type TxClient } from "@cvsa/core/common"; export class SongRepository implements ISongRepository { constructor(private readonly prisma: PrismaClient) {} - async getById(id: SongId) { - return this.prisma.song.findFirst({ where: { id, deletedAt: null } }); + async getById(id: SongId, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.song.findFirst({ where: { id, deletedAt: null } }) + ); } - async getDetailsById(id: SongId): Promise { - const data = await this.prisma.song.findFirst({ - where: { id, deletedAt: null }, - include: { - performances: { - include: { - singer: true, + async getDetailsById(id: SongId, tx?: TxClient): Promise { + const client = tx ?? this.prisma; + const data = transformPrismaResult( + await client.song.findFirst({ + where: { id, deletedAt: null }, + include: { + performances: { + include: { + singer: true, + svsEngine: true, + svsEngineVersion: true, + }, }, - }, - creations: { - include: { - role: true, - artist: true, + creations: { + include: { + role: true, + artist: true, + }, }, + lyrics: true, }, - lyrics: true, - }, - }); + }) + ); if (!data) return null; const { performances, creations, lyrics, ...song } = data; return { - singers: performances.map((item) => item.singer), + singers: performances.map((item) => { + const engineName = item.svsEngine?.name ?? ""; + const engineVersion = item.svsEngineVersion?.versionString ?? ""; + const engine = (() => { + if (!engineName) return null; + if (!engineVersion) return engineName; + return `${engineName} ${engineVersion}`; + })(); + return { + ...item.singer, + engine, + }; + }), artists: creations.map((item) => { return { ...item.artist, @@ -49,77 +74,46 @@ export class SongRepository implements ISongRepository { }; } - async list(query: ListSongsQueryDto = {}) { - const { offset: skip = 0, limit: take = 50, type, search } = query; - - const where: Prisma.SongWhereInput = { - deletedAt: null, - type, - }; - - if (search) { - where.OR = [ - { name: { contains: search, mode: "insensitive" } }, - { description: { contains: search, mode: "insensitive" } }, - ]; - } - - const [songs, total] = await Promise.all([ - this.prisma.song.findMany({ - where, - skip, - take, - orderBy: { createdAt: "desc" }, - }), - this.prisma.song.count({ where }), - ]); - - return { songs, total }; - } - - async create(input: CreateSongRequestDto) { + async create(input: CreateSongRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; const { performances, creations, ...songData } = input; - return this.prisma.song.create({ - data: { - type: songData.type ?? null, - name: songData.name ?? null, - duration: songData.duration ?? null, - description: songData.description ?? null, - coverUrl: songData.coverUrl ?? null, - publishedAt: songData.publishedAt ?? null, - performances: performances && { - create: performances, - }, - creations: creations && { - create: creations.map((c) => ({ - artistId: c.artistId, - artistRoleId: c.roleId, - })), + return transformPrismaResult( + await client.song.create({ + data: { + type: songData.type ?? null, + name: songData.name ?? null, + duration: songData.duration ?? null, + description: songData.description ?? null, + coverUrl: songData.coverUrl ?? null, + publishedAt: songData.publishedAt ?? null, + performances: performances && { + create: performances, + }, + creations: creations && { + create: creations.map((c) => ({ + artistId: c.artistId, + artistRoleId: c.roleId, + })), + }, }, - }, - }); + }) + ); } - async update(id: SongId, input: UpdateSongRequestDto) { - const songData = input; - - const data: Prisma.SongUpdateInput = { - ...(songData.type !== undefined ? { type: songData.type } : {}), - ...(songData.name !== undefined ? { name: songData.name } : {}), - ...(songData.duration !== undefined ? { duration: songData.duration } : {}), - ...(songData.description !== undefined ? { description: songData.description } : {}), - ...(songData.coverUrl !== undefined ? { coverUrl: songData.coverUrl } : {}), - ...(songData.publishedAt !== undefined ? { publishedAt: songData.publishedAt } : {}), - }; + async update(id: SongId, input: UpdateSongRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; - return this.prisma.song.update({ - where: { id }, - data, - }); + return transformPrismaResult( + await client.song.update({ + where: { id }, + data: input, + }) + ); } - async softDelete(id: SongId) { - await this.prisma.song.update({ where: { id }, data: { deletedAt: new Date() } }); + async softDelete(id: SongId, tx?: TxClient) { + const client = tx ?? this.prisma; + await client.song.update({ where: { id }, data: { deletedAt: new Date() } }); } } diff --git a/packages/core/modules/catalog/song/service.ts b/packages/core/modules/catalog/song/service.ts index e227dbc..5da0f71 100644 --- a/packages/core/modules/catalog/song/service.ts +++ b/packages/core/modules/catalog/song/service.ts @@ -1,20 +1,21 @@ -import { prisma } from "@cvsa/core/common"; +import { AppError, type SongSearchService } from "@cvsa/core"; +import type { Song } from "@cvsa/db"; import type { SongDetailsResponseDto, SongId, - ListSongsQueryDto, - ListSongsResponseDto, + CreateSongRequestDto, + UpdateSongRequestDto, } from "./dto"; -import type { CreateSongRequestDto, UpdateSongRequestDto } from "./dto"; -import { SongRepository } from "./repository"; -import { AppError } from "@cvsa/core"; -import type { Song } from "@cvsa/db"; -import { pick, map } from "remeda"; +import type { ISongRepository } from "./repository.interface"; +import type { IDetailsService, Serialized } from "@cvsa/core/common"; -export class SongService { - constructor(private readonly repository: SongRepository = new SongRepository(prisma)) {} +export class SongService implements IDetailsService { + constructor( + private readonly repository: ISongRepository, + private readonly search: SongSearchService + ) {} - async getDetails(id: SongId): Promise { + async getDetails(id: SongId) { const result = await this.repository.getDetailsById(id); if (result === null) { throw new AppError("Song not found", "NOT_FOUND", 404); @@ -22,16 +23,28 @@ export class SongService { return result; } - async create(input: CreateSongRequestDto): Promise { - return this.repository.create(input); + async create(input: CreateSongRequestDto): Promise> { + const result = await this.repository.create(input); + try { + this.search.sync(result.id); + } catch (e) { + console.error(e); + } + return result; } - async update(id: SongId, input: UpdateSongRequestDto): Promise { + async update(id: SongId, input: UpdateSongRequestDto): Promise> { const existing = await this.repository.getById(id); if (existing === null) { throw new AppError("Song not found", "NOT_FOUND", 404); } - return this.repository.update(id, input); + const result = await this.repository.update(id, input); + try { + this.search.sync(id); + } catch (e) { + console.error(e); + } + return result; } async delete(id: SongId): Promise { @@ -40,19 +53,10 @@ export class SongService { throw new AppError("Song not found", "NOT_FOUND", 404); } await this.repository.softDelete(id); - } - - async list(query: ListSongsQueryDto = {}): Promise { - const { offset = 0, limit = 50 } = query; - const result = await this.repository.list(query); - return { - songs: map( - result.songs, - pick(["id", "type", "name", "duration", "coverUrl", "publishedAt"]) - ), - total: result.total, - offset, - limit, - }; + try { + this.search.sync(id); + } catch (e) { + console.error(e); + } } } diff --git a/packages/core/modules/index.ts b/packages/core/modules/index.ts index 25b4339..74a7cd7 100644 --- a/packages/core/modules/index.ts +++ b/packages/core/modules/index.ts @@ -1,2 +1,3 @@ export * from "./auth"; export * from "./catalog"; +export * from "./search"; diff --git a/packages/core/modules/internal.ts b/packages/core/modules/internal.ts new file mode 100644 index 0000000..422fc78 --- /dev/null +++ b/packages/core/modules/internal.ts @@ -0,0 +1,3 @@ +export * from "./catalog"; +export * from "./search"; +export * from "./ml"; diff --git a/packages/core/modules/ml/embedding.ts b/packages/core/modules/ml/embedding.ts new file mode 100644 index 0000000..446dd89 --- /dev/null +++ b/packages/core/modules/ml/embedding.ts @@ -0,0 +1,124 @@ +import path from "node:path"; +import { AutoTokenizer, type PreTrainedTokenizer } from "@huggingface/transformers"; +import { downloadFile } from "@huggingface/hub"; +import * as ort from "onnxruntime-node"; + +const modelDir = path.join(import.meta.dir, "../../../../model/"); + +// TODO: More formalized config +const tokenizerModel = "minishlab/potion-multilingual-128M"; +const modelPath = path.join(modelDir, "./potion-quant/model.onnx"); + +// TODO: Unit test for this +export class EmbeddingManager { + private tokenizer: PreTrainedTokenizer | null = null; + private session: ort.InferenceSession | null = null; + + public async init(): Promise { + await this.downloadModel(); + await this.initTokenizer(); + await this.initSession(); + } + + private async initTokenizer(): Promise { + if (this.tokenizer !== null) { + return; + } + this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerModel); + } + + private async initSession(): Promise { + if (this.session !== null) { + return; + } + this.session = await ort.InferenceSession.create(modelPath); + } + + private async getTokenizer(): Promise { + if (this.tokenizer === null) { + this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerModel); + } + return this.tokenizer; + } + + private async getModelSession(): Promise { + if (this.session === null) { + this.session = await ort.InferenceSession.create(modelPath); + } + return this.session; + } + + private reshape(data: Float16Array, shape: number[]) { + const [rows, cols] = shape; + const result = []; + + for (let i = 0; i < rows; i++) { + const start = i * cols; + const end = start + cols; + + result.push(Array.from(data.subarray(start, end))); + } + + return result; + } + + // TODO: progress indicator + private async downloadModel() { + const modelFile = Bun.file(modelPath); + if (await modelFile.exists()) { + return; + } + console.log("Downloading embedding model..."); + const blob = await downloadFile({ + repo: "alikia2x/potion-multilingual-128M-int8", + path: "onnx/model.onnx", + }); + if (!blob) { + throw new Error("Cannot download model file."); + } + const buffer = await blob.arrayBuffer(); + console.log("Embedding model downloaded."); + modelFile.write(buffer); + } + + public async getEmbedding(texts: string[]): Promise { + const tokenizer = await this.getTokenizer(); + const session = await this.getModelSession(); + + const { input_ids } = await tokenizer(texts, { + add_special_tokens: false, + return_tensor: false, + }); + + const cumsum = (arr: number[]): number[] => { + const result: number[] = new Array(arr.length); + let currentSum = 0; + for (let i = 0; i < arr.length; i++) { + currentSum += arr[i]; + result[i] = currentSum; + } + return result; + }; + + const offsets: number[] = [ + 0, + ...cumsum(input_ids.slice(0, -1).map((x: string) => x.length)), + ]; + const flattened_input_ids = input_ids.flat(); + + const inputs = { + input_ids: new ort.Tensor("int64", new BigInt64Array(flattened_input_ids.map(BigInt)), [ + flattened_input_ids.length, + ]), + offsets: new ort.Tensor("int64", new BigInt64Array(offsets.map(BigInt)), [ + offsets.length, + ]), + }; + + const { embeddings } = await session.run(inputs); + return this.reshape(embeddings.data as unknown as Float16Array, [...embeddings.dims]); + } +} + +export const embeddingManager = new EmbeddingManager(); +await embeddingManager.init(); diff --git a/packages/core/modules/ml/index.ts b/packages/core/modules/ml/index.ts new file mode 100644 index 0000000..5b00d36 --- /dev/null +++ b/packages/core/modules/ml/index.ts @@ -0,0 +1 @@ +export * from "./embedding"; diff --git a/packages/core/modules/search/catalog/index.ts b/packages/core/modules/search/catalog/index.ts new file mode 100644 index 0000000..153b6ce --- /dev/null +++ b/packages/core/modules/search/catalog/index.ts @@ -0,0 +1 @@ +export * from "./song"; diff --git a/packages/core/modules/search/catalog/song.ts b/packages/core/modules/search/catalog/song.ts new file mode 100644 index 0000000..9f1059a --- /dev/null +++ b/packages/core/modules/search/catalog/song.ts @@ -0,0 +1,129 @@ +import type { SongType } from "@cvsa/db"; +import { ISearchService } from "../search.interface"; +import { unique, keys } from "remeda"; +import type { SongDetailsResponseDto } from "@cvsa/core"; + +export interface SongSearchIndex { + id: number; + name?: string; + lyrics?: string; + description?: string; + singers?: string[]; + artists?: string[]; + bilibiliAid?: number; + bilibiliBvid?: string; + type?: SongType; + tags?: string[]; + engine?: string[]; + publishedAt?: number; + bilibiliViews?: number; + _vectors?: { + "potion-multilingual-128M": number[]; + }; +} + +// TODO: Integration test +export class SongSearchService extends ISearchService { + private async getDocument( + song: SongDetailsResponseDto, + language: string + ): Promise { + if (!this.embeddingManager) { + // TODO: Use unified Error object + throw new Error("Embedding service not available"); + } + const getDesc = () => { + if (language === song.language) return song.description; + return song.localizedDescriptions?.[language]; + }; + const getLyrics = () => { + return song.lyrics.find((item) => item.language === language)?.plainText; + }; + const getLocalizedName = (attr: "singers" | "artists") => { + const data = song[attr].map((item) => { + if (item.language === language) { + return item.name; + } + return item.localizedNames?.[language]; + }); + return data.filter(Boolean) as string[]; + }; + const getSingers = () => { + return getLocalizedName("singers"); + }; + const getArtists = () => { + return getLocalizedName("artists"); + }; + const vectors = await this.embeddingManager.getEmbedding([ + `Name: ${song.name ?? ""} +Lyrics: ${song.lyrics ?? ""} +Description: ${getDesc() ?? ""} +Singers: ${getSingers().join(", ")} +Artists: ${getArtists().join(", ")} +`, + ]); + return { + id: song.id, + name: song.name ?? undefined, + lyrics: getLyrics() ?? undefined, + description: getDesc() ?? undefined, + singers: getSingers(), + artists: getArtists(), + bilibiliAid: Number(song.bilibiliAid) ?? undefined, + bilibiliBvid: song.bilibiliBvid ?? undefined, + type: song.type ?? undefined, + engine: song.singers + .map((item) => item.engine ?? undefined) + .filter(Boolean) as string[], + publishedAt: song.publishedAt ? new Date(song.publishedAt).getTime() : undefined, + _vectors: { + "potion-multilingual-128M": vectors[0], + }, + }; + } + + public async sync(id: number) { + if (!this.manager) { + throw new Error("Search service not available"); + } + const song = await this.repository.getDetailsById(id); + + if (!song) { + const indexesToBeDeleted = await this.manager.getLocalizedIndexesOfEntity("song"); + for (const index of indexesToBeDeleted) { + await this.manager.getAdminIndex(index).deleteDocument(id); + } + return; + } + + const languages = unique([ + ...keys(song.localizedNames ?? []), + ...keys(song.localizedDescriptions ?? []), + song.language, + ]); + for (const language of languages) { + const indexUid = `song_${language}`; + const index = this.manager.getAdminIndex(indexUid); + const document = await this.getDocument(song, language); + index.addDocuments([document], { + primaryKey: "id", + }); + } + } + + public async search(query: string, language: string = "zh") { + if (!this.manager || !this.embeddingManager) { + throw new Error("Search or embedding service not available"); + } + + const index = this.manager.getSearchIndex(`song_${language}`); + return index.search(query, { + vector: (await this.embeddingManager.getEmbedding([query]))[0], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + } +} diff --git a/packages/core/modules/search/config.ts b/packages/core/modules/search/config.ts new file mode 100644 index 0000000..2f8d436 --- /dev/null +++ b/packages/core/modules/search/config.ts @@ -0,0 +1,50 @@ +import type { Settings } from "meilisearch"; +// import { env } from "@cvsa/core/common"; + +export const INDEX_SETTINGS: Record = { + song: { + searchableAttributes: [ + "name", + "lyrics", + "description", + "singers", + "artists", + "bilibiliAid", + "bilibiliBvid", + ], + filterableAttributes: ["type", "tags", "singers", "engine"], + sortableAttributes: ["publishedAt", "bilibiliViews"], + rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], + embedders: { + "potion-multilingual-128M": { + source: "userProvided", + dimensions: 256, + }, + // TODO: Remove this + // "qwen3-embedding-8b-openrouter": { + // source: "rest", + // url: "https://openrouter.ai/api/v1/embeddings", + // request: { + // model: "qwen/qwen3-embedding-8b", + // input: ["{{text}}", "{{..}}"], + // encoding_format: "float", + // }, + // response: { + // data: [{ embedding: "{{embedding}}" }, "{{..}}"], + // }, + // apiKey: env.OPENROUTER_KEY, + // dimensions: 4096, + // documentTemplate: `Instruct: You are providing search results for songs in the database. + // Query: This song has the following properties. + // Name: {{doc.name}}, + + // Singers: {% for singer in doc.singers %} + // {{ singer }} + // {% endfor %} + // Artists: {% for artist in doc.artists %} + // {{ artist }} + // {% endfor %}`, + // }, + }, + }, +}; diff --git a/packages/core/modules/search/container.ts b/packages/core/modules/search/container.ts new file mode 100644 index 0000000..5727a55 --- /dev/null +++ b/packages/core/modules/search/container.ts @@ -0,0 +1,3 @@ +import { SearchManager } from "./manager"; + +export const searchManager = await SearchManager.create(); diff --git a/packages/core/modules/search/index.ts b/packages/core/modules/search/index.ts new file mode 100644 index 0000000..5bb3214 --- /dev/null +++ b/packages/core/modules/search/index.ts @@ -0,0 +1,2 @@ +export * from "./catalog"; +export * from "./container"; diff --git a/packages/core/modules/search/manager.ts b/packages/core/modules/search/manager.ts new file mode 100644 index 0000000..0c483ed --- /dev/null +++ b/packages/core/modules/search/manager.ts @@ -0,0 +1,183 @@ +import { MeiliSearch, type Settings, type RecordAny } from "meilisearch"; +import { env } from "@cvsa/core"; +import { INDEX_SETTINGS } from "./config"; +import { deepEqualUnordered } from "@cvsa/core/common"; + +interface SearchManagerConstructor { + client: MeiliSearch; + adminClient: MeiliSearch; +} + +// TODO: Error handling & integration test +export class SearchManager { + private readonly client: MeiliSearch; + private readonly adminClient: MeiliSearch; + + private constructor({ client, adminClient }: SearchManagerConstructor) { + this.client = client; + this.adminClient = adminClient; + this.syncAllSettings(); + } + + /** + * Creates a new SearchManager instance with separate search and admin clients. + * Initializes MeiliSearch clients using API keys retrieved from the master client. + * The search client has only search permissions, while the admin client has full permissions. + * @returns A promise that resolves to a new SearchManager instance + */ + public static async create() { + try { + const masterClient = new MeiliSearch({ + host: env.MEILI_API_URL, + apiKey: env.MEILI_MASTER_KEY, + }); + const adminKey = await SearchManager.getAdminKey(masterClient); + const searchKey = await SearchManager.getSearchKey(masterClient); + const client = new MeiliSearch({ + host: env.MEILI_API_URL, + apiKey: searchKey, + }); + const adminClient = new MeiliSearch({ + host: env.MEILI_API_URL, + apiKey: adminKey, + }); + return new SearchManager({ client, adminClient }); + } catch { + return; + } + } + + private static async getOrCreateKey( + client: MeiliSearch, + params: { actions: string[]; indexes: string[]; name: string } + ): Promise { + const { results: keys } = await client.getKeys({ limit: 1000 }); + + const existingKey = keys.find( + (key) => + deepEqualUnordered(key.actions, params.actions) && + deepEqualUnordered(key.indexes, params.indexes) + ); + + if (existingKey) { + return existingKey.key; + } + + const newKey = await client.createKey({ + ...params, + expiresAt: null, + }); + + return newKey.key; + } + + private static async getAdminKey(client: MeiliSearch): Promise { + return SearchManager.getOrCreateKey(client, { + actions: ["*"], + indexes: ["*"], + name: "Admin API Key", + }); + } + + private static async getSearchKey(client: MeiliSearch): Promise { + return SearchManager.getOrCreateKey(client, { + actions: ["search"], + indexes: ["*"], + name: "Search API Key", + }); + } + + /** + * Waits for a MeiliSearch task to complete. + * @param taskUid - The unique identifier of the task to wait for + * @returns A promise that resolves to the completed task + */ + public async waitForTask(taskUid: number) { + return await this.adminClient.tasks.waitForTask(taskUid, { + timeout: 10000, + interval: 100, + }); + } + + /** + * Retrieves a specific task by its UID. + * @param taskUid - The unique identifier of the task + * @returns A promise that resolves to the task details + */ + public async getTask(taskUid: number) { + return await this.adminClient.tasks.getTask(taskUid); + } + + /** + * Returns an admin-index for the specified index name. + * Admin index has full permissions including write and delete operations. + * @param indexName - The name of the index + * @returns The admin MeiliSearch index instance + */ + public getAdminIndex(indexName: string) { + return this.adminClient.index(indexName); + } + + /** + * Returns a search-index for the specified index name. + * Search index has read-only permissions for search operations only. + * @param indexName - The name of the index + * @returns The search-only MeiliSearch index instance + */ + public getSearchIndex(indexName: string) { + return this.client.index(indexName); + } + + /** + * Lists all indexes in the MeiliSearch instance. + * @returns A promise that resolves to the list of all indexes + */ + public async listIndexes() { + return this.adminClient.getIndexes(); + } + + /** + * Gets all localized indexes for a given entity name. + * + * Expects index UIDs in the format `{entityName}_{locale}` (e.g., `artist_zh-CN`). + * @param name - The entity name to filter indexes by + * @returns A promise that resolves to an array of matching index UIDs + */ + public async getLocalizedIndexesOfEntity(name: string) { + const { results: indexes } = await this.adminClient.getIndexes({ + limit: 1000, + }); + const results: string[] = []; + for (const index of indexes) { + const [indexName, _] = index.uid.split("_"); + if (indexName === name) results.push(index.uid); + } + return results; + } + + /** + * Synchronizes settings for all indexes with the configured INDEX_SETTINGS. + * Only updates settings that differ from current values and triggers reindex + * for settings that require it (non-displayedAttributes and non-rankingRules). + * Skips indexes without a corresponding entry in INDEX_SETTINGS. + */ + public async syncAllSettings() { + const { results: indexes } = await this.listIndexes(); + for (const index of indexes) { + const uid = index.uid; + const [name, lang] = uid.split("_"); + if (lang === undefined) continue; + if (!(name in INDEX_SETTINGS)) continue; + const settings = INDEX_SETTINGS[name as keyof typeof INDEX_SETTINGS]; + const currentSettings = await index.getSettings(); + for (const [key, value] of Object.entries(settings)) { + if (deepEqualUnordered(value, currentSettings[key as keyof Settings])) continue; + // These settings do not trigger a full reindex + if (["displayedAttributes", "rankingRules"].includes(key)) continue; + await index.resetSettings(); + await index.updateSettings(settings); + break; + } + } + } +} diff --git a/packages/core/modules/search/search.interface.ts b/packages/core/modules/search/search.interface.ts new file mode 100644 index 0000000..6d164c0 --- /dev/null +++ b/packages/core/modules/search/search.interface.ts @@ -0,0 +1,14 @@ +import type { IDetailsRepository } from "@cvsa/core/common"; +import type { SearchManager } from "./manager"; +import type { SearchResponse } from "meilisearch"; +import type { EmbeddingManager } from "@cvsa/core/modules"; + +export abstract class ISearchService { + constructor( + protected readonly repository: IDetailsRepository, + protected readonly manager?: SearchManager, + protected readonly embeddingManager?: EmbeddingManager + ) {} + abstract sync(id: number): Promise; + abstract search(query: string, language: string): Promise; +} diff --git a/packages/core/package.json b/packages/core/package.json index d2ae194..c14787c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -17,14 +17,19 @@ }, "exports": { ".": "./index.ts", - "./common": "./common/internal.ts" + "./common": "./common/internal.ts", + "./modules": "./modules/internal.ts" }, "dependencies": { "@better-auth/prisma-adapter": "^1.5.5", "@cvsa/db": "*", + "@huggingface/hub": "^2.11.0", + "@huggingface/transformers": "^4.0.0", "@t3-oss/env-core": "^0.13.10", "better-auth": "^1.5.5", + "meilisearch": "^0.56.0", "nanoid": "^5.1.6", + "onnxruntime-node": "^1.24.3", "remeda": "^2.33.6", "zod": "^4.3.6" } diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index f321ed3..1db6ca1 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -1,17 +1,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; -import { PrismaClient } from "@cvsa/db"; -import { PrismaPg } from "@prisma/adapter-pg"; -import { SongRepository } from "@cvsa/core"; -import type { CreateSongRequestDto, UpdateSongRequestDto } from "@cvsa/core"; -import { env } from "@cvsa/core/common"; - -const prisma = new PrismaClient({ - adapter: new PrismaPg({ - connectionString: env.DATABASE_URL, - }), -}); +import { songRepository, type CreateSongRequestDto, type UpdateSongRequestDto } from "@cvsa/core"; +import { prisma } from "@cvsa/core/common"; -const repository = new SongRepository(prisma); +const repository = songRepository; describe("SongRepository Integration Tests", () => { beforeAll(async () => { @@ -35,7 +26,7 @@ describe("SongRepository Integration Tests", () => { duration: 180, description: "A test song", coverUrl: "https://example.com/cover.jpg", - publishedAt: new Date("2024-01-01"), + publishedAt: new Date("2024-01-01").toISOString(), }; const result = await repository.create(input); @@ -81,67 +72,6 @@ describe("SongRepository Integration Tests", () => { }); }); - describe("list", () => { - test("should return all songs when no query provided", async () => { - await repository.create({ name: "Song 1" }); - await repository.create({ name: "Song 2" }); - await repository.create({ name: "Song 3" }); - - const result = await repository.list(); - - expect(result.songs.length).toBe(3); - }); - - test("should filter by type", async () => { - await repository.create({ name: "Original Song", type: "ORIGINAL" }); - await repository.create({ name: "Cover Song", type: "COVER" }); - - const result = await repository.list({ type: "ORIGINAL" }); - - expect(result.songs.length).toBe(1); - expect(result.songs[0].type).toBe("ORIGINAL"); - }); - - test("should filter by search term in name", async () => { - await repository.create({ name: "Searchable Song" }); - await repository.create({ name: "Another Song" }); - - const result = await repository.list({ search: "Search" }); - - expect(result.songs.length).toBe(1); - expect(result.songs[0].name).toBe("Searchable Song"); - }); - - test("should filter by search term in description", async () => { - await repository.create({ name: "Song A", description: "Unique description here" }); - await repository.create({ name: "Song B", description: "Different content" }); - - const result = await repository.list({ search: "Unique" }); - - expect(result.songs.length).toBe(1); - expect(result.songs[0].name).toBe("Song A"); - }); - - test("should apply pagination with offset and limit", async () => { - for (let i = 1; i <= 10; i++) { - await repository.create({ name: `Song ${i}` }); - } - - const result = await repository.list({ offset: 2, limit: 3 }); - - expect(result.songs.length).toBe(3); - }); - - test("should exclude soft-deleted songs", async () => { - const song = await repository.create({ name: "To Delete" }); - await repository.softDelete(song.id); - - const result = await repository.list(); - - expect(result.songs.length).toBe(0); - }); - }); - describe("update", () => { test("should update all fields", async () => { const created = await repository.create({ name: "Old Name", type: "ORIGINAL" }); @@ -182,9 +112,6 @@ describe("SongRepository Integration Tests", () => { const result = await repository.getById(created.id); expect(result).toBeNull(); - - const allSongs = await repository.list(); - expect(allSongs.songs.length).toBe(0); }); }); }); diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index 50cf6c6..476f6f5 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -1,50 +1,65 @@ -import { describe, expect, test, mock } from "bun:test"; +import { describe, expect, mock, test } from "bun:test"; import { SongService, AppError } from "@cvsa/core"; -import type { SongRepository, SongDetailsResponseDto } from "@cvsa/core"; +import type { + SongDetailsResponseDto, + ISongRepository, + SongSearchService, + Serialized, +} from "@cvsa/core"; +import type { Song } from "@cvsa/db"; +import { createMockRepository } from "../utils"; -describe("SongService", () => { - const mockGetDetailsById = mock(); - const mockRepository = { - getDetailsById: mockGetDetailsById, - } as unknown as SongRepository; - const songService = new SongService(mockRepository); +const mockSongDetails: SongDetailsResponseDto = { + id: 1, + type: "ORIGINAL", + name: "Test Song", + language: "zh", + duration: 180, + description: "A test song", + coverUrl: "https://example.com/cover.jpg", + publishedAt: new Date("2024-01-01").toISOString(), + deletedAt: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + originalSongId: null, + localizedNames: {}, + localizedDescriptions: {}, + bilibiliAid: null, + bilibiliBvid: null, + vocadbId: null, + vcpediaId: null, + moegirlId: null, + singers: [], + artists: [], + lyrics: [], +}; - const mockSongDetails: SongDetailsResponseDto = { - id: 1, - type: "ORIGINAL", - name: "Test Song", - duration: 180, - description: "A test song", - coverUrl: "https://example.com/cover.jpg", - publishedAt: new Date("2024-01-01"), - deletedAt: null, - createdAt: new Date(), - updatedAt: new Date(), - originalSongId: null, - localizedNames: {}, - localizedDescriptions: {}, - bilibiliAid: null, - bilibiliBvid: null, - vocadbId: null, - vcpediaId: null, - moegirlId: null, - singers: [], - artists: [], - lyrics: [] +describe("SongService", () => { + const mockRepository = createMockRepository({ + getById: async () => null, + getDetailsById: async () => mockSongDetails, + create: async () => Promise.resolve(null as unknown as Serialized), + update: async () => Promise.resolve(null as unknown as Serialized), + softDelete: async () => {}, + }); + const mockSearchService = { + sync: mock(async (_id: number) => {}), }; + const songService = new SongService( + mockRepository as unknown as ISongRepository, + mockSearchService as unknown as SongSearchService + ); describe("getDetails", () => { test("returns song details when song exists", async () => { - mockGetDetailsById.mockResolvedValue(mockSongDetails); - const result = await songService.getDetails(1); expect(result).toEqual(mockSongDetails); - expect(mockGetDetailsById).toHaveBeenCalledWith(1); + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); }); test("throws NOT_FOUND error when song does not exist", async () => { - mockGetDetailsById.mockResolvedValue(null); + mockRepository.getDetailsById.mockResolvedValue(null); expect(songService.getDetails(999)).rejects.toThrow(AppError); expect(songService.getDetails(999)).rejects.toThrow("Song not found"); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts new file mode 100644 index 0000000..bde370c --- /dev/null +++ b/packages/core/tests/utils.ts @@ -0,0 +1,13 @@ +import { mock } from "bun:test"; + +export function createMockRepository(template: T) { + const mockRepo = {} as Record>; + + for (const key of Object.keys(template) as (keyof T)[]) { + if (typeof template[key] === "function") { + mockRepo[key] = mock(template[key] as never); + } + } + + return mockRepo; +} diff --git a/packages/db/index.ts b/packages/db/index.ts index ce4ab2f..f2fe784 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -2,3 +2,4 @@ export * from "./prisma/generated/client"; export * from "./prisma/generated/models"; export * from "./prisma/generated/enums"; export * from "./prisma/zod"; +export * from "./prisma/types"; diff --git a/packages/db/package.json b/packages/db/package.json index 4d8504d..5f8bdea 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -8,12 +8,12 @@ "migrate:dev": "bunx --bun prisma migrate dev", "push": "bunx --bun prisma db push", "db:deploy": "bunx --bun prisma migrate deploy", - "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", + "test:setup": "NODE_ENV=test bunx --bun prisma db push --accept-data-loss", "studio": "bunx --bun prisma studio", "typecheck": "bunx --bun tsc --noEmit" }, "devDependencies": { - "@cvsa/prisma-zod": "^3.3.0", + "@cvsa/prisma-zod": "^3.6.0", "@cvsa/typescript-config": "workspace:*", "prisma": "^7.6.0", "prisma-json-types-generator": "^4.1.1" diff --git a/packages/db/prisma/models/core/album.prisma b/packages/db/prisma/models/core/album.prisma index 2c22677..c850552 100644 --- a/packages/db/prisma/models/core/album.prisma +++ b/packages/db/prisma/models/core/album.prisma @@ -6,6 +6,7 @@ model Album { description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") + language String @default("zh") songInAlbums SongInAlbum[] coverUrl String? @map("cover_url") createdAt DateTime @default(now()) @map("created_at") diff --git a/packages/db/prisma/models/core/artist.prisma b/packages/db/prisma/models/core/artist.prisma index b57bf59..f691a40 100644 --- a/packages/db/prisma/models/core/artist.prisma +++ b/packages/db/prisma/models/core/artist.prisma @@ -3,6 +3,7 @@ model Artist { name String? /// [LocalizedField] localizedNames Json? @map("localized_names") + language String @default("zh") aliases String[] description String? /// [LocalizedField] diff --git a/packages/db/prisma/models/core/lyrics.prisma b/packages/db/prisma/models/core/lyrics.prisma index 337c975..7524a83 100644 --- a/packages/db/prisma/models/core/lyrics.prisma +++ b/packages/db/prisma/models/core/lyrics.prisma @@ -2,7 +2,7 @@ model Lyrics { id Int @id @default(autoincrement()) song Song @relation(fields: [songId], references: [id]) songId Int @map("song_id") - language String? + language String @default("zh") isTranslated Boolean @default(false) @map("is_translated") plainText String? @map("plain_text") ttml String? diff --git a/packages/db/prisma/models/core/singer.prisma b/packages/db/prisma/models/core/singer.prisma index 278c17b..b876ff9 100644 --- a/packages/db/prisma/models/core/singer.prisma +++ b/packages/db/prisma/models/core/singer.prisma @@ -2,6 +2,7 @@ model Singer { id Int @id @default(autoincrement()) name String? avatarUrl String? @map("avatar_url") + language String @default("zh") /// [LocalizedField] localizedNames Json? @map("localized_names") description String? diff --git a/packages/db/prisma/models/core/song.prisma b/packages/db/prisma/models/core/song.prisma index 2c0b589..6ad9f4f 100644 --- a/packages/db/prisma/models/core/song.prisma +++ b/packages/db/prisma/models/core/song.prisma @@ -4,12 +4,13 @@ model Song { originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id]) originalSongId Int? @map("original_song_id") derivations Song[] @relation(name: "originalSong") + language String @default("zh") name String? + /// [LocalizedField] + localizedNames Json? @map("localized_names") duration Int? lyrics Lyrics[] songTags SongTag[] - /// [LocalizedField] - localizedNames Json? @map("localized_names") description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") diff --git a/packages/db/prisma/models/core/songSeries.prisma b/packages/db/prisma/models/core/songSeries.prisma index 414cadb..ebc8896 100644 --- a/packages/db/prisma/models/core/songSeries.prisma +++ b/packages/db/prisma/models/core/songSeries.prisma @@ -6,6 +6,7 @@ model SongSeries { description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") + language String @default("zh") songInSeries SongInSeries[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b9618ee..06d7cad 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -13,8 +13,11 @@ datasource db { } generator zod { - provider = "prisma-zod" - output = "./zod" + provider = "prisma-zod" + output = "./zod" + extendSchema = "zodSchema" + dateSchema = "string" + bigintSchema = "number" } /* diff --git a/packages/db/prisma/types.ts b/packages/db/prisma/types.ts index 6121293..b1c34e2 100644 --- a/packages/db/prisma/types.ts +++ b/packages/db/prisma/types.ts @@ -1,10 +1,8 @@ +import type { z } from "zod"; +import type { LocalizedField } from "./zodSchema"; + declare global { namespace PrismaJson { - type LocalizedField = { - [lang: string]: string; - }; + type LocalizedField = z.infer; } } - -// This file must be a module. -export {}; diff --git a/packages/db/prisma/zodSchema.ts b/packages/db/prisma/zodSchema.ts new file mode 100644 index 0000000..f4a0db2 --- /dev/null +++ b/packages/db/prisma/zodSchema.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const LocalizedField = z.record(z.string(), z.string()); diff --git a/packages/db/schema.xml b/packages/db/schema.xml deleted file mode 100644 index a472bc8..0000000 --- a/packages/db/schema.xml +++ /dev/null @@ -1,955 +0,0 @@ -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - - -This section contains a summary of this file. - - -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - - - -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Repository files (if enabled) -5. Multiple file entries, each consisting of: - - File path as an attribute - - Full contents of the file - - - -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - - - -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - - - - - -prisma/ - models/ - auth/ - account.prisma - README.md - session.prisma - user.prisma - verification.prisma - core/ - album.prisma - artist.prisma - artistRole.prisma - externalLink.prisma - group.prisma - lyrics.prisma - README.md - singer.prisma - song.prisma - songSeries.prisma - svsEngine.prisma - svsEngineVersion.prisma - tag.prisma - voicebank.prisma - meta/ - history.prisma - permission.prisma - README.md - role.prisma - platform/ - file.prisma - post.prisma - README.md - reputationHistory.prisma - models.prisma - relations/ - core/ - creation.prisma - performance.prisma - songInAlbum.prisma - songInSeries.prisma - songTag.prisma - meta/ - rolePermission.prisma - README.md - relations.prisma - schema.prisma - types.ts -.env.example -.gitignore -index.ts -package.json -prisma.config.ts -README.md -tsconfig.json - - - -This section contains the contents of the repository's files. - - -model Account { - id String @id - accountId String - providerId String - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - accessToken String? - refreshToken String? - idToken String? - accessTokenExpiresAt DateTime? - refreshTokenExpiresAt DateTime? - scope String? - password String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@map("account") - @@schema("auth") -} - - - -# Auth Schema - -This schema handles Identity and Access Management (IAM). - -- **User:** The central account entity. -- **Session:** Tracks active logins, device information. Used in authorization. - - - -model Session { - id String @id - token String - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - ipAddress String? @map("ip_address") - userAgent String? @map("user_agent") - expiresAt DateTime - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@unique([token]) - @@index([userId]) - @@map("session") - @@schema("auth") -} - - - -model User { - id String @id - name String? - username String @unique - displayUsername String @unique @map("display_username") - email String? @unique - emailVerified Boolean @default(false) @map("email_verified") - image String? - changesMade History[] - role Role? @relation(fields: [roleId], references: [id]) - roleId Int? @map("role_id") - sessions Session[] - accounts Account[] - artist Artist? - reputation Int @default(0) - reputationHistories ReputationHistory[] - posts Post[] - files File[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("user") - @@schema("auth") -} - - - -model Verification { - id String @id - identifier String - value String - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([identifier]) - @@map("verification") - @@schema("auth") -} - - - -model Album { - id Int @id @default(autoincrement()) - name String? - /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - songInAlbums SongInAlbum[] - coverUrl String? @map("cover_url") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("album") - @@schema("core") -} - - - -model Artist { - id Int @id @default(autoincrement()) - name String? - /// [LocalizedField] - localizedNames Json? @map("localized_names") - aliases String[] - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - creations Creation[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User? @relation(fields: [userId], references: [id]) - userId String? @unique @map("user_id") - deletedAt DateTime? @map("deleted_at") - - @@map("artist") - @@schema("core") -} - - - -model ArtistRole { - id Int @id @default(autoincrement()) - role String - /// [LocalizedField] - localizedRoles Json? @map("localized_roles") - creations Creation[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("artist_role") - @@schema("core") -} - - - -model ExternalLink { - id Int @id @default(autoincrement()) - label String? - url String - platform Platform? - platformId String? @map("platform_id") - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("external_link") - @@schema("core") -} - -enum Platform { - YOUTUBE - NICONICO - BILIBILI - VOCALOID_WIKI - VOCALOID_LYRICS_WIKI - SOUNDCLOUD - NETEASE_MUSIC - QQ_MUSIC - FIVE_SING // 5sing.kugou.com - KUGOU - SPOTIFY - APPLE_MUSIC - YOUTUBE_MUSIC - WIKIPEDIA - BAIDU_BAIKE - PIXIV - WEIBO - TWITTER - VOCADB - MOEGIRLPEDIA - MUSICBRAINZ - VCPEDIA - XIAOHONGSHU // aka RedNote - - @@map("third_party_platform") - @@schema("core") -} - - - -model Group { - id Int @id @default(autoincrement()) - name String - description String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("group") - @@schema("core") -} - - - -model Lyrics { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - language String? - isTranslated Boolean @default(false) @map("is_translated") - plainText String? @map("plain_text") - ttml String? - lrc String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("lyrics") - @@schema("core") -} - - - -# Core Schema - -The `core` schema is the heart of the CVSA database. It functions as a structured encyclopedia for the Chinese Singing Voice Synthesis community. - -## Design Philosophy - -The data here is designed to be **objective and universal**. It models the relationships between music, creators, etc. - -## Sub-domains - -1. **Music Entities:** Songs, Albums, Lyrics, and Song Series. -2. **Talent:** Artists (producers/illustrators) and Singers (virtual characters). -3. **Classification:** A hierarchical Tag system for genre and thematic categorization. - -## Localization - -Most entities use `LocalizedField` (JSON) to support multi-language names and descriptions (e.g., Chinese, Japanese, and English). - - - -model Singer { - id Int @id @default(autoincrement()) - name String? - avatarUrl String? @map("avatar_url") - /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - voicebanks Voicebank[] - performances Performance[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("singer") - @@schema("core") -} - - - -model Song { - id Int @id @default(autoincrement()) - type SongType? - originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id]) - originalSongId Int? @map("original_song_id") - derivations Song[] @relation(name: "originalSong") - name String? - duration Int? - lyrics Lyrics[] - songTags SongTag[] - /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - songInAlbums SongInAlbum[] - bilibiliAid BigInt? @map("bilibili_aid") - bilibiliBvid String? @map("bilibili_bvid") - vocadbId Int? @map("vocadb_id") - vcpediaId Int? @map("vcpedia_id") - moegirlId Int? @map("moegirl_id") - externalLinks ExternalLink[] - coverUrl String? @map("cover_url") - creations Creation[] - performances Performance[] - songInSeries SongInSeries[] - publishedAt DateTime? @map("published_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("song") - @@schema("core") -} - -enum SongType { - ORIGINAL - COVER - REMIX - REMASTER - MASHUP - INSTRUMENTAL - OTHERS - - @@map("song_type") - @@schema("core") -} - - - -model SongSeries { - id Int @id @default(autoincrement()) - name String? - /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - songInSeries SongInSeries[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("song_series") - @@schema("core") -} - - - -model SvsEngine { - id Int @id @default(autoincrement()) - name String - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - svsEngineVersions SvsEngineVersion[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - performances Performance[] - - @@map("svs_engine") - @@schema("core") -} - - - -model SvsEngineVersion { - id Int @id @default(autoincrement()) - versionString String @map("version_string") - svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int @map("svs_engine_id") - voicebanks Voicebank[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - performances Performance[] - - @@map("svs_engine_version") - @@schema("core") -} - - - -model Tag { - id Int @id @default(autoincrement()) - name String - /// [LocalizedField] - localizedNames Json? @map("localized_names") - songTags SongTag[] - parent Tag? @relation(name: "tagTree", fields: [parentId], references: [id]) - parentId Int? @map("parent_id") - children Tag[] @relation(name: "tagTree") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("tag") - @@schema("core") -} - - - -model Voicebank { - id Int @id @default(autoincrement()) - singer Singer @relation(fields: [singerId], references: [id]) - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - singerId Int @map("singer_id") - language String - svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) - svsEngineVersionId Int @map("svs_engine_version_id") - performances Performance[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("voicebank") - @@schema("core") -} - - - -model History { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - table String - objectId Int @map("object_id") - type ChangeType - comment String? - old Json? - new Json? - createdAt DateTime @default(now()) @map("created_at") - - @@map("history") - @@schema("meta") -} - -enum ChangeType { - CREATE - UPDATE - DELETE - RESTORE - - @@map("change_type") - @@schema("meta") -} - - - -model Permission { - id Int @id @default(autoincrement()) - action String - roles RolePermission[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("permission") - @@schema("meta") -} - - - -# Meta Schema - -The `meta` schema manages system governance, auditing, and configuration. - -- **Auditing:** The `History` table tracks every change made to the `core` entities (Create/Update/Delete) for rollback and accountability. -- **RBAC:** Implements Role-Based Access Control via `Role` and `Permission` entities. - - - -model Role { - id Int @id @default(autoincrement()) - name String @unique - /// [LocalizedField] - localizedNames Json? @map("localized_names") - permissions RolePermission[] - users User[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("role") - @@schema("meta") -} - - - -model File { - id String @id - originalName String? @map("original_name") - path String? - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - - @@map("file") - @@schema("platform") -} - - - -model Post { - id String @id - text String? - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("post") - @@schema("platform") -} - - - -# Platform Schema - -This schema contains tables and logic specific to the **CVSA as a platform**. - -## Purpose - -While the `core` schema focuses on universal SVS (Singing Voice Synthesis) data that could theoretically be shared across different encyclopedia projects, the `platform` schema manages data unique to this specific implementation. - -It includes: - -- **User Activity:** Personal notes, bookmarks, and interaction history. -- **System State:** File storage metadata and upload tracking. - -and so on. - - - -model ReputationHistory { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - actionType String @map("action_type") - points Int - sourceType Int @map("source_type") - sourceId Int @map("source_id") - createdAt DateTime @default(now()) @map("created_at") - - @@map("reputation_history") - @@schema("platform") -} - - - -/* -NOTE: -This is an empty placeholder file to ensure the Prisma Language Server functions -correctly, serving as a temporary workaround. - -See the link below for details: -https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600 - -This issue persists as of Prisma v7.3.0, which was the latest -version at the time of writing. -*/ - - - -model Creation { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - artist Artist @relation(fields: [artistId], references: [id]) - artistId Int @map("artist_id") - role ArtistRole @relation(fields: [artistRoleId], references: [id]) - artistRoleId Int @map("artist_role_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("creation") - @@schema("core") -} - - - -// Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.). -// In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese). -// That's why we need to have both SvsEngine and SvsEngineVersion. -model Performance { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - singer Singer @relation(fields: [singerId], references: [id]) - singerId Int @map("singer_id") - voicebank Voicebank? @relation(fields: [voicebankId], references: [id]) - voicebankId Int? @map("voicebank_id") - svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int? @map("svs_engine_id") - svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id]) - svsEngineVersionId Int? @map("svs_engine_version_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("performance") - @@schema("core") -} - - - -model SongInAlbum { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - album Album @relation(fields: [albumId], references: [id]) - albumId Int @map("album_id") - trackNumber Int @map("track_number") - discNumber Int @default(1) @map("disc_number") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("song_in_album") - @@schema("core") -} - - - -model SongInSeries { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - series SongSeries @relation(fields: [seriesId], references: [id]) - seriesId Int @map("series_id") - order Int - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("song_in_series") - @@schema("core") -} - - - -model SongTag { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - tag Tag @relation(fields: [tagId], references: [id]) - tagId Int @map("tag_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("song_tag") - @@schema("core") -} - - - -model RolePermission { - id Int @id @default(autoincrement()) - role Role @relation(fields: [roleId], references: [id]) - roleId Int @map("role_id") - permission Permission @relation(fields: [permissionId], references: [id]) - permissionId Int @map("permission_id") - - @@map("role_permission") - @@schema("meta") -} - - - -# Relations - -This directory contains join tables for Many-to-Many relationships. - -To maintain a clean and modular Prisma schema, we separate core data models (in `/models`) from their association entities. -This structure helps prevent individual `.prisma` files from becoming too large and complex. - - - -/* -NOTE: -This is an empty placeholder file to ensure the Prisma Language Server functions -correctly, serving as a temporary workaround. - -See the link below for details: -https://github.com/prisma/prisma/discussions/24413#discussioncomment-9670600 - -This issue persists as of Prisma v7.3.0, which was the latest -version at the time of writing. -*/ - - - -generator client { - provider = "prisma-client" - output = "./generated" -} - -generator json { - provider = "prisma-json-types-generator" -} - -datasource db { - provider = "postgresql" - schemas = ["core", "meta", "platform", "auth"] -} - -generator zod { - provider = "prisma-zod" - output = "./zod" -} - -/* -# CVSA (Chinese Vocal Synthesis Archive) Database Schema - -## Overview -The CVSA is a structured encyclopedia and data platform dedicated to the Chinese Vocal Synthesis (SVS/VC) community. -It aims to bridge the gap between traditional wikis (like MediaWiki) and structured databases by providing: -- Automation: Fully automated song discovery, metadata extraction, and statistical tracking. -- Structured Data: Moving beyond flat text to rich, relational data for songs, artists, engines, and albums. -- Collaboration: A platform for contributors to provide descriptive content, translations, and corrections. - -## Schemas -The database is partitioned into several PostgreSQL schemas. - -### A. [core] - The Encyclopedia -This is the heart of the project. It stores universal SVS data that remains objective across platforms. -- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), -SVS Engines (Vocaloid, Synthesizer V), Voicebanks, Albums, and Tags. -- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata -(Simplified Chinese, Traditional Chinese, Japanese, English, etc.). -- Versioning: Supports tracking of specific SVS engine versions and voicebank iterations. - -### B. [platform] - The Service Layer -Contains data specific to the CVSA website as a functional service. -- Features: User-uploaded files, personal notes, etc. -- Scope: Data here is specific to this implementation and not necessarily shared with other archives. - -### C. [auth] - Identity & Access -Handles user accounts, session persistence, and secure authentication. -- RBAC: Connects users to roles defined in the `meta` schema. - -### D. [meta] - Governance & Auditing -Manages the "data about data." -- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) -in the `core` schema for accountability and rollbacks. -- Permissions: Granular action-based permissions assigned to roles. - -## 3. Technical Constraints & Design Patterns -- Soft Deletion: Most core entities implement a `deletedAt` timestamp for soft-deletion support. -- Modular Relations: Many-to-Many relations are extracted into separate files -within the `/relations` directory to keep entity definitions clean and prevent Prisma file bloat. - -## 4. Current Status & TODO -- Indexing: Performance indexes and unique constraints beyond primary keys are still under design. -- Constraints: Complex business logic constraints (e.g., preventing circular tag parents) are handled at the application layer. - -*/ - - - -declare global { - namespace PrismaJson { - type LocalizedField = { - [lang: string]: string; - }; - } -} - -// This file must be a module. -export {}; - - - -DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa - - - -prisma/generated -prisma/zod - - - -export * from "./prisma/generated/client"; -export * from "./prisma/generated/models"; -export * from "./prisma/generated/enums"; -export * from "./prisma/zod"; - - - -{ - "name": "@cvsa/db", - "type": "module", - "module": "index.ts", - "scripts": { - "gen": "bunx --bun prisma generate", - "migrate": "bunx --bun prisma migrate dev --create-only", - "migrate:dev": "bunx --bun prisma migrate dev", - "push": "bunx --bun prisma db push", - "db:deploy": "bunx --bun prisma migrate deploy", - "test:setup": "NODE_ENV=test bunx --bun prisma migrate reset --force", - "studio": "bunx --bun prisma studio", - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@cvsa/prisma-zod": "^3.3.0", - "@cvsa/typescript-config": "workspace:*", - "prisma": "^7.6.0", - "prisma-json-types-generator": "^4.1.1" - }, - "dependencies": { - "@prisma/adapter-pg": "^7.6.0", - "@prisma/client": "^7.6.0", - "@types/pg": "^8.16.0", - "zod": "^4.3.6" - } -} - - - -// This file was generated by Prisma, and assumes you run Prisma commands using `bun --bun run prisma [command]`. -import { defineConfig, env } from "prisma/config"; - -export default defineConfig({ - schema: "prisma/", - migrations: { - path: "prisma/migrations", - }, - datasource: { - url: env("DATABASE_URL"), - }, -}); - - - -# @cvsa/db - -## Available Scripts - -- `gen` – Generate Prisma client and schema artifacts -- `deploy` – Deploy database changes -- `migrate` – Create a new migration file without applying it -- `migrate:dev` – Apply migrations to the development database -- `push` – Push schema changes directly to the database -- `test:setup` – Reset and apply migrations for test environment -- `studio` – Open Prisma Studio database GUI - - - -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["index.ts"] -} - - - diff --git a/turbo.json b/turbo.json index b5a5c23..922fba8 100644 --- a/turbo.json +++ b/turbo.json @@ -16,6 +16,22 @@ "lint": {}, "test": {}, "test:coverage": {}, - "db:deploy": {} + "setup": { + "cache": false, + "dependsOn": ["@cvsa/db#push", "@cvsa/db#test:setup", "@cvsa/db#gen"] + }, + "setup:ci": { + "cache": false, + "dependsOn": ["@cvsa/db#test:setup", "@cvsa/db#gen"] + }, + "@cvsa/db#test:setup": { + "cache": false + }, + "@cvsa/db#push": { + "cache": false + }, + "@cvsa/db#gen": { + "cache": false + } } } From fdace6b79af6b180dce9e3c37783b470e0db44ca Mon Sep 17 00:00:00 2001 From: ji233 <27987772@qq.com> Date: Sat, 4 Apr 2026 22:38:57 +0800 Subject: [PATCH 11/37] feat(song): support creating song with lyrics (#38) --- apps/backend/tests/song.test.ts | 44 +++++++++++++++++++ packages/core/modules/catalog/song/dto.ts | 11 ++++- .../core/modules/catalog/song/repository.ts | 5 ++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts index f3994a4..634cb7b 100644 --- a/apps/backend/tests/song.test.ts +++ b/apps/backend/tests/song.test.ts @@ -9,6 +9,7 @@ describe("Song E2E Tests", () => { beforeEach(async () => { await prisma.session.deleteMany(); await prisma.user.deleteMany(); + await prisma.lyrics.deleteMany(); await prisma.song.deleteMany(); }); @@ -64,6 +65,49 @@ describe("Song E2E Tests", () => { code: "UNAUTHORIZED", }); }); + + test("should create a song with lyrics", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Lyrics Song", + type: "ORIGINAL" as const, + lyrics: [ + { + language: "zh", + isTranslated: false, + plainText: "第一行歌词\n第二行歌词", + lrc: "[00:00.00]第一行歌词\n[00:05.00]第二行歌词", + ttml: "", + }, + ], + }; + + const { data, status } = await api.v2.song.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Lyrics Song", + }); + + // Verify lyrics were persisted to DB + const lyrics = await prisma.lyrics.findMany({ + where: { songId: data?.id }, + }); + expect(lyrics).toHaveLength(1); + expect(lyrics[0]).toMatchObject({ + language: "zh", + isTranslated: false, + plainText: "第一行歌词\n第二行歌词", + lrc: "[00:00.00]第一行歌词\n[00:05.00]第二行歌词", + ttml: "", + }); + }); }); describe("PATCH /v2/song/:id - Update Song", () => { diff --git a/packages/core/modules/catalog/song/dto.ts b/packages/core/modules/catalog/song/dto.ts index a27f3e1..99aaae6 100644 --- a/packages/core/modules/catalog/song/dto.ts +++ b/packages/core/modules/catalog/song/dto.ts @@ -16,15 +16,24 @@ const CreatePerformanceSchema = z.object({ svsEngineVersionId: z.int().positive().nullish(), }); +const CreateLyricsSchema = z.object({ + language: z.string().optional(), + isTranslated: z.boolean().optional(), + plainText: z.string().optional(), + ttml: z.string().optional(), + lrc: z.string().optional(), +}); + export const CreateSongRequestSchema = z.object({ type: SongTypeSchema.nullish(), name: z.string().nullish(), - duration: z.number().nullish(), + duration: z.int().nullish(), description: z.string().nullish(), coverUrl: z.url().nullish(), publishedAt: z.iso.datetime().nullish(), performances: z.array(CreatePerformanceSchema).optional(), creations: z.array(CreateCreationSchema).optional(), + lyrics: z.array(CreateLyricsSchema).optional(), }); export const UpdateSongRequestSchema = z.object({ diff --git a/packages/core/modules/catalog/song/repository.ts b/packages/core/modules/catalog/song/repository.ts index 7d26cc3..84e7eb7 100644 --- a/packages/core/modules/catalog/song/repository.ts +++ b/packages/core/modules/catalog/song/repository.ts @@ -76,7 +76,7 @@ export class SongRepository implements ISongRepository { async create(input: CreateSongRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - const { performances, creations, ...songData } = input; + const { performances, creations, lyrics, ...songData } = input; return transformPrismaResult( await client.song.create({ @@ -96,6 +96,9 @@ export class SongRepository implements ISongRepository { artistRoleId: c.roleId, })), }, + lyrics: lyrics && { + create: lyrics, + }, }, }) ); From 99b8d492a9cae8b3e762bfbd1161f86f9394bf41 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 5 Apr 2026 01:35:48 +0800 Subject: [PATCH 12/37] refactor: observability, log & i18n support (#39) Introduce tracing and structured logging to improve observability and debuggability of the system. - Extract environment configuration, logging, and observability concerns into dedicated packages (`@cvsa/env`, `@cvsa/logger`, `@cvsa/observability`), and move the embedding inference server into its own `@cvsa/embedding` package. - Introduce a Pino-based request logger and an application logger that bridges to OpenTelemetry. - Add an i18n middleware for per-request locale detection, replace hard-coded error message strings with i18n keys, and include trace IDs in error responses via X-Trace-ID headers. - Refactor the error handler into a composable Elysia plugin and restructure packages/core source files under a src/ directory. - Add locale bundles for English and Chinese backend error messages, and expose development-only endpoints for heap snapshot inspection and GC triggering. --- .agents/skills/jujutsu-vcs/SKILL.md | 7 +- .opencode/commands/commit.md | 9 + apps/backend/package.json | 18 +- .../src/common/error/getErrorResponse.ts | 14 +- apps/backend/src/common/i18n.ts | 19 + apps/backend/src/common/log.ts | 50 ++ apps/backend/src/common/middlewares/index.ts | 1 - apps/backend/src/common/trace.ts | 28 ++ apps/backend/src/errorHandler.ts | 208 ++++---- .../{modules => handlers}/auth/currentUser.ts | 10 +- .../src/{modules => handlers}/auth/index.ts | 0 .../src/{modules => handlers}/auth/login.ts | 20 +- .../src/{modules => handlers}/auth/logout.ts | 12 +- .../src/{modules => handlers}/auth/signup.ts | 14 +- .../catalog/song/create.ts | 9 +- .../catalog/song/delete.ts | 18 +- .../catalog/song/details.ts | 8 +- .../catalog/song/index.ts | 0 .../catalog/song/search.ts | 8 +- .../catalog/song/update.ts | 19 +- apps/backend/src/handlers/dev/gc.ts | 12 + apps/backend/src/handlers/dev/index.ts | 7 + apps/backend/src/handlers/dev/snapshot.ts | 13 + .../src/{modules => handlers}/index.ts | 1 + apps/backend/src/index.ts | 23 +- .../src/{common => }/middlewares/auth.ts | 2 +- apps/backend/src/middlewares/i18n.ts | 42 ++ apps/backend/src/middlewares/index.ts | 3 + apps/backend/src/middlewares/requestLogger.ts | 56 +++ apps/backend/src/onAfterHandle.ts | 42 +- apps/backend/src/startMessage.ts | 2 +- apps/backend/tests/auth.test.ts | 2 +- apps/backend/tests/song.test.ts | 2 +- apps/backend/tsconfig.json | 2 +- bun.lock | 461 +++++++++++++++--- locale/manifest.json | 4 + locale/modules/backend/manifest.json | 4 + locale/modules/backend/messages/en.json | 9 + locale/modules/backend/messages/zh.json | 9 + packages/core/common/index.ts | 4 - packages/core/common/internal.ts | 10 - packages/core/common/schema/index.ts | 1 - packages/core/common/types/repository.ts | 5 - packages/core/index.ts | 2 - packages/core/modules/catalog/song/service.ts | 62 --- packages/core/modules/internal.ts | 3 - packages/core/modules/ml/index.ts | 1 - .../core/modules/search/search.interface.ts | 14 - packages/core/package.json | 28 +- .../core/{modules => src}/auth/betterAuth.ts | 4 +- packages/core/{modules => src}/auth/index.ts | 0 .../core/{modules => src}/auth/user/dto.ts | 4 +- .../core/{modules => src}/auth/user/index.ts | 0 .../core/{modules => src}/catalog/index.ts | 0 .../catalog/song/container.ts | 8 +- .../core/{modules => src}/catalog/song/dto.ts | 10 +- .../{modules => src}/catalog/song/index.ts | 0 .../catalog/song/repository.interface.ts | 6 +- .../catalog/song/repository.ts | 2 +- packages/core/src/catalog/song/service.ts | 73 +++ .../core/{common => src}/error/AppError.ts | 0 packages/core/{common => src}/error/index.ts | 1 + .../schema/error.ts => src/error/schema.ts} | 2 + packages/core/{modules => src}/index.ts | 3 + packages/core/src/internal.ts | 6 + .../{modules => src}/search/catalog/index.ts | 0 .../{modules => src}/search/catalog/song.ts | 36 +- .../core/{modules => src}/search/config.ts | 0 .../core/{modules => src}/search/container.ts | 0 .../core/{modules => src}/search/index.ts | 1 + packages/core/src/search/interface.ts | 14 + .../core/{modules => src}/search/manager.ts | 4 +- packages/core/{common => src}/types/index.ts | 1 - packages/core/src/types/repository.ts | 5 + .../core/{common => src}/types/service.ts | 2 +- .../utils/deepEqualUnordered.ts | 0 packages/core/{common => src}/utils/index.ts | 0 .../core/{common => src}/utils/randomId.ts | 0 .../tests/integration/SongRepository.test.ts | 2 +- packages/core/tests/unit/SongService.test.ts | 12 +- packages/core/tsconfig.json | 2 +- packages/core/turbo.json | 8 +- packages/db/index.ts | 5 - packages/db/package.json | 7 +- .../types/TxClient.ts => db/src/index.ts} | 5 +- packages/{core/common => db/src}/prisma.ts | 14 +- packages/db/src/types.ts | 5 + packages/db/tsconfig.json | 2 +- packages/embedding/package.json | 24 + .../modules/ml => embedding/src}/embedding.ts | 85 +++- packages/embedding/src/index.ts | 35 ++ packages/embedding/tests/e2e.test.ts | 18 + packages/embedding/tsconfig.json | 4 + packages/env/package.json | 18 + .../{core/common/env.ts => env/src/index.ts} | 7 +- packages/env/tsconfig.json | 4 + packages/logger/package.json | 29 ++ packages/logger/src/appLogger.ts | 140 ++++++ packages/logger/src/index.ts | 11 + packages/logger/src/pinoSetup.ts | 49 ++ packages/logger/src/types.ts | 52 ++ packages/logger/tsconfig.json | 4 + packages/observability/package.json | 21 + packages/observability/src/index.ts | 2 + packages/observability/src/init.ts | 20 + packages/observability/src/tracer.ts | 18 + packages/observability/tsconfig.json | 4 + turbo.json | 20 +- 108 files changed, 1683 insertions(+), 424 deletions(-) create mode 100644 .opencode/commands/commit.md create mode 100644 apps/backend/src/common/i18n.ts create mode 100644 apps/backend/src/common/log.ts delete mode 100644 apps/backend/src/common/middlewares/index.ts create mode 100644 apps/backend/src/common/trace.ts rename apps/backend/src/{modules => handlers}/auth/currentUser.ts (57%) rename apps/backend/src/{modules => handlers}/auth/index.ts (100%) rename apps/backend/src/{modules => handlers}/auth/login.ts (71%) rename apps/backend/src/{modules => handlers}/auth/logout.ts (59%) rename apps/backend/src/{modules => handlers}/auth/signup.ts (74%) rename apps/backend/src/{modules => handlers}/catalog/song/create.ts (62%) rename apps/backend/src/{modules => handlers}/catalog/song/delete.ts (53%) rename apps/backend/src/{modules => handlers}/catalog/song/details.ts (60%) rename apps/backend/src/{modules => handlers}/catalog/song/index.ts (100%) rename apps/backend/src/{modules => handlers}/catalog/song/search.ts (54%) rename apps/backend/src/{modules => handlers}/catalog/song/update.ts (57%) create mode 100644 apps/backend/src/handlers/dev/gc.ts create mode 100644 apps/backend/src/handlers/dev/index.ts create mode 100644 apps/backend/src/handlers/dev/snapshot.ts rename apps/backend/src/{modules => handlers}/index.ts (70%) rename apps/backend/src/{common => }/middlewares/auth.ts (87%) create mode 100644 apps/backend/src/middlewares/i18n.ts create mode 100644 apps/backend/src/middlewares/index.ts create mode 100644 apps/backend/src/middlewares/requestLogger.ts create mode 100644 locale/manifest.json create mode 100644 locale/modules/backend/manifest.json create mode 100644 locale/modules/backend/messages/en.json create mode 100644 locale/modules/backend/messages/zh.json delete mode 100644 packages/core/common/index.ts delete mode 100644 packages/core/common/internal.ts delete mode 100644 packages/core/common/schema/index.ts delete mode 100644 packages/core/common/types/repository.ts delete mode 100644 packages/core/index.ts delete mode 100644 packages/core/modules/catalog/song/service.ts delete mode 100644 packages/core/modules/internal.ts delete mode 100644 packages/core/modules/ml/index.ts delete mode 100644 packages/core/modules/search/search.interface.ts rename packages/core/{modules => src}/auth/betterAuth.ts (93%) rename packages/core/{modules => src}/auth/index.ts (100%) rename packages/core/{modules => src}/auth/user/dto.ts (97%) rename packages/core/{modules => src}/auth/user/index.ts (100%) rename packages/core/{modules => src}/catalog/index.ts (100%) rename packages/core/{modules => src}/catalog/song/container.ts (55%) rename packages/core/{modules => src}/catalog/song/dto.ts (93%) rename packages/core/{modules => src}/catalog/song/index.ts (100%) rename packages/core/{modules => src}/catalog/song/repository.interface.ts (70%) rename packages/core/{modules => src}/catalog/song/repository.ts (97%) create mode 100644 packages/core/src/catalog/song/service.ts rename packages/core/{common => src}/error/AppError.ts (100%) rename packages/core/{common => src}/error/index.ts (51%) rename packages/core/{common/schema/error.ts => src/error/schema.ts} (85%) rename packages/core/{modules => src}/index.ts (50%) create mode 100644 packages/core/src/internal.ts rename packages/core/{modules => src}/search/catalog/index.ts (100%) rename packages/core/{modules => src}/search/catalog/song.ts (82%) rename packages/core/{modules => src}/search/config.ts (100%) rename packages/core/{modules => src}/search/container.ts (100%) rename packages/core/{modules => src}/search/index.ts (65%) create mode 100644 packages/core/src/search/interface.ts rename packages/core/{modules => src}/search/manager.ts (98%) rename packages/core/{common => src}/types/index.ts (67%) create mode 100644 packages/core/src/types/repository.ts rename packages/core/{common => src}/types/service.ts (55%) rename packages/core/{common => src}/utils/deepEqualUnordered.ts (100%) rename packages/core/{common => src}/utils/index.ts (100%) rename packages/core/{common => src}/utils/randomId.ts (100%) delete mode 100644 packages/db/index.ts rename packages/{core/common/types/TxClient.ts => db/src/index.ts} (53%) rename packages/{core/common => db/src}/prisma.ts (78%) create mode 100644 packages/db/src/types.ts create mode 100644 packages/embedding/package.json rename packages/{core/modules/ml => embedding/src}/embedding.ts (56%) create mode 100644 packages/embedding/src/index.ts create mode 100644 packages/embedding/tests/e2e.test.ts create mode 100644 packages/embedding/tsconfig.json create mode 100644 packages/env/package.json rename packages/{core/common/env.ts => env/src/index.ts} (59%) create mode 100644 packages/env/tsconfig.json create mode 100644 packages/logger/package.json create mode 100644 packages/logger/src/appLogger.ts create mode 100644 packages/logger/src/index.ts create mode 100644 packages/logger/src/pinoSetup.ts create mode 100644 packages/logger/src/types.ts create mode 100644 packages/logger/tsconfig.json create mode 100644 packages/observability/package.json create mode 100644 packages/observability/src/index.ts create mode 100644 packages/observability/src/init.ts create mode 100644 packages/observability/src/tracer.ts create mode 100644 packages/observability/tsconfig.json diff --git a/.agents/skills/jujutsu-vcs/SKILL.md b/.agents/skills/jujutsu-vcs/SKILL.md index 883f821..19e65c5 100644 --- a/.agents/skills/jujutsu-vcs/SKILL.md +++ b/.agents/skills/jujutsu-vcs/SKILL.md @@ -88,10 +88,11 @@ jj log --limit n # Show recent n commits jj log --revisions @ --no-graph --template "description" # See description of working copy # Diff (always use --git or -s to prevent GUI hang) -jj diff --git # Show diff of working copy vs parent -jj diff --git -r x # Show diff between x and its parent +# We exclude lock files using filesets syntax +jj diff --git '~*.lock' # Show diff of working copy vs parent +jj diff --git -r x '~*.lock' # Show diff between x and its parent jj diff --git [paths] # Show diff for specific paths -jj diff --git --from x --to y # Show diff from x to y +jj diff --git --from x --to y '~*.lock' # Show diff from x to y jj diff -s # Show only whether modified, added, or deleted # Show commit info (use --summary to prevent GUI hang) diff --git a/.opencode/commands/commit.md b/.opencode/commands/commit.md new file mode 100644 index 0000000..b88191d --- /dev/null +++ b/.opencode/commands/commit.md @@ -0,0 +1,9 @@ +--- +description: Write a commit message +--- + +Use VCS tools to check what has changed in the current commit, then write a commit message. + +The commit message should follow the 50/70 rule and describe both the motivation for the commit and the changes it involves. + +After drafting your first version, present it to the user and modify it if they give you any feedback. diff --git a/apps/backend/package.json b/apps/backend/package.json index 59bafdb..4f0f331 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,13 +1,13 @@ { "name": "@cvsa/backend", "type": "module", - "version": "0.0.1", + "version": "2.1.0", "scripts": { "lint": "bunx --bun biome lint", - "dev": "bun --hot src/index.ts", + "dev": "NODE_ENV=development bun --watch src/index.ts", "start": "bun src/index.ts", - "test": "bun test", - "test:coverage": "bun test --coverage", + "test": "NODE_ENV=test bun test", + "test:coverage": "NODE_ENV=test bun test --coverage", "typecheck": "bunx --bun tsc --noEmit" }, "devDependencies": { @@ -19,8 +19,15 @@ "@better-auth/prisma-adapter": "^1.5.4", "@cvsa/core": "workspace:*", "@cvsa/db": "workspace:*", - "@elysiajs/eden": "^1.0.5", + "@cvsa/env": "workspace:*", + "@cvsa/logger": "workspace:*", + "@elysiajs/eden": "^1.4.9", "@elysiajs/openapi": "^1.4.14", + "@elysiajs/opentelemetry": "^1.4.10", + "@fanee/runtime-node": "^0.2.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", + "@opentelemetry/sdk-trace-node": "^2.6.1", "@prisma/adapter-pg": "^7.4.1", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", @@ -30,6 +37,7 @@ "elysia-ip": "^1.0.10", "elysia-rate-limit": "^4.5.0", "nanoid": "^5.1.6", + "pino": "^10.3.1", "zod": "^4.3.6" } } diff --git a/apps/backend/src/common/error/getErrorResponse.ts b/apps/backend/src/common/error/getErrorResponse.ts index af86344..677127e 100644 --- a/apps/backend/src/common/error/getErrorResponse.ts +++ b/apps/backend/src/common/error/getErrorResponse.ts @@ -1,4 +1,6 @@ import type { ErrorResponseDto } from "@cvsa/core"; +import { i18nRuntime } from "@/common/i18n"; +import { getTraceId } from "@/common/trace"; // biome-ignore lint/suspicious/noExplicitAny: Utility type type StatusFunc = (code: number, response: any) => any; @@ -6,7 +8,17 @@ type StatusFunc = (code: number, response: any) => any; export const getErrorResponse = ( statusFunc: T, statusCode: number, + locale: string | undefined, data: ErrorResponseDto ): ReturnType => { - return statusFunc(statusCode, data); + const traceId = getTraceId(); + const { message } = data; + const translatedMessages = message ? i18nRuntime.tAll(message) : undefined; + const translatedMessage = message ? i18nRuntime.t({ locale })(message) : undefined; + return statusFunc(statusCode, { + ...data, + message: translatedMessage, + i18n: locale === undefined ? translatedMessages : undefined, + traceId, + }); }; diff --git a/apps/backend/src/common/i18n.ts b/apps/backend/src/common/i18n.ts new file mode 100644 index 0000000..c82184d --- /dev/null +++ b/apps/backend/src/common/i18n.ts @@ -0,0 +1,19 @@ +import { env } from "@cvsa/env"; +import { FaneeRuntime } from "@fanee/runtime-node"; +import path from "node:path"; + +const getBundleLocation = () => { + if (env.NODE_ENV !== "production") { + return path.join(import.meta.dir, "../../../../locale/"); + } else { + return path.join(import.meta.dir, "./locale/"); + } +}; + +export const i18nRuntime: FaneeRuntime = new FaneeRuntime({ + bundlePath: getBundleLocation(), + defaultLocale: "zh", + namespace: "backend", +}); + +await i18nRuntime.load(); diff --git a/apps/backend/src/common/log.ts b/apps/backend/src/common/log.ts new file mode 100644 index 0000000..d9582bc --- /dev/null +++ b/apps/backend/src/common/log.ts @@ -0,0 +1,50 @@ +import chalk from "chalk"; + +export function getLogLevelForRequest(status: number): "error" | "warn" | "info" { + if (status >= 500) return "error"; + if (status >= 400) return "warn"; + return "info"; +} + +interface GinLogData { + method?: string; + path?: string; + status?: number; + latency?: string; + ip?: string; +} + +export function formatGinLog(data: GinLogData = {}): string { + const parts: string[] = []; + + if (data.method) { + const m = data.method.toUpperCase(); + let methodBg = chalk.bgBlue.white.bold; + if (m === "POST") methodBg = chalk.bgCyan.black.bold; + else if (m === "PUT") methodBg = chalk.bgYellow.black.bold; + else if (m === "DELETE") methodBg = chalk.bgRed.white.bold; + else if (m === "PATCH") methodBg = chalk.bgGreen.white.bold; + parts.push(methodBg(` ${m.padEnd(6)} `)); + } + + if (data.status) { + let statusColor = chalk.green; + if (data.status >= 400 && data.status < 500) statusColor = chalk.yellow; + else if (data.status >= 500) statusColor = chalk.red; + parts.push(statusColor(data.status.toString().padStart(3))); + } + + if (data.latency !== undefined) { + parts.push(chalk.magenta(data.latency.padStart(10))); + } + + if (data.ip) { + parts.push(chalk.gray(data.ip.padStart(15))); + } + + if (data.path) { + parts.push(`"${data.path}"`); + } + + return parts.join(" | "); +} diff --git a/apps/backend/src/common/middlewares/index.ts b/apps/backend/src/common/middlewares/index.ts deleted file mode 100644 index 97ccf76..0000000 --- a/apps/backend/src/common/middlewares/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./auth"; diff --git a/apps/backend/src/common/trace.ts b/apps/backend/src/common/trace.ts new file mode 100644 index 0000000..9be36ee --- /dev/null +++ b/apps/backend/src/common/trace.ts @@ -0,0 +1,28 @@ +import { SpanStatusCode, trace, context } from "@opentelemetry/api"; +import pkg from "../../package.json"; + +export const tracer = trace.getTracer(pkg.name, pkg.version); + +export async function traceTask(name: string, fn: () => T): Promise { + return tracer.startActiveSpan(name, async (span) => { + try { + span.setStatus({ code: SpanStatusCode.OK }); + return await fn(); + } catch (e) { + span.setStatus({ code: SpanStatusCode.ERROR }); + throw e; + } finally { + span.end(); + } + }); +} + +export const getTraceId = () => { + const currentSpan = trace.getSpan(context.active()); + + if (currentSpan) { + const spanContext = currentSpan.spanContext(); + + return spanContext.traceId; + } +}; diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index 9ac2c36..f984aad 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -1,8 +1,12 @@ -import { APIError } from "better-auth"; -import type { ErrorHandler } from "elysia"; -import { AppError } from "@cvsa/core"; +import { Elysia, ElysiaCustomStatusResponse } from "elysia"; +import { AppError, BetterAuthAPIError } from "@cvsa/core"; +import { appLogger, pinoLogger } from "@cvsa/logger"; import type { ErrorResponseDto } from "@cvsa/core"; import { getErrorResponse } from "@/common/error"; +import { getTraceId } from "@/common/trace"; +import { i18nMiddleware } from "@/middlewares"; +import { ip } from "elysia-ip"; +import { formatGinLog, getLogLevelForRequest } from "@common/log"; const AUTH_CONFLICT_CODES = [ "USERNAME_IS_ALREADY_TAKEN", @@ -19,85 +23,125 @@ const AUTH_INVALID_CODES = [ type AuthConflictCode = (typeof AUTH_CONFLICT_CODES)[number]; type AuthInvalidCode = (typeof AUTH_INVALID_CODES)[number]; -type StatusDecorator = ReturnType>["status"]; - -const response = { - appError: (status: StatusDecorator, error: AppError) => - getErrorResponse(status, error.statusCode, { - code: error.code as ErrorResponseDto["code"], - message: error.message, - }), - - notFound: (status: StatusDecorator) => - getErrorResponse(status, 404, { - code: "NOT_FOUND", - message: "The requested resource was not found.", - }), - - validation: ( - status: StatusDecorator, - error: { message: string; detail: (msg: string) => unknown } - ) => { - const detail = error.detail(error.message); - return getErrorResponse(status, 422, { - code: "VALIDATION_ERROR", - message: typeof detail === "string" ? detail : (detail as { summary: string }).summary, - }); - }, - - betterAuthConflict: (status: StatusDecorator, error: APIError) => - getErrorResponse(status, 409, { - code: (error.body?.code || "ENTITY_CONFLICT") as ErrorResponseDto["code"], - message: error.body?.message, - }), - - betterAuthInvalidCred: (status: StatusDecorator) => - getErrorResponse(status, 401, { - code: "INVALID_CREDENTIALS", - message: "Provided credentials are invalid.", - }), - - betterAuthGeneral: (status: StatusDecorator, error: APIError) => - getErrorResponse(status, error.statusCode, { - code: (error.body?.code || "AUTH_ERROR") as ErrorResponseDto["code"], - message: error.body?.message, - }), - - internal: (status: StatusDecorator) => - getErrorResponse(status, 500, { - code: "INTERNAL_SERVER_ERROR", - message: "Internal server error", - }), -} as const; - -export const errorHandler: ErrorHandler<{ - readonly AppError: AppError; -}> = ({ code, status, error }) => { - if (AppError.isAppError(error)) { - return response.appError(status, error); - } - - if (code === "NOT_FOUND") { - return response.notFound(status); - } - - if (code === "VALIDATION") { - return response.validation(status, error); - } - - if (error instanceof APIError) { - const bodyCode = error.body?.code || ""; - - if (AUTH_CONFLICT_CODES.includes(bodyCode as AuthConflictCode)) { - return response.betterAuthConflict(status, error); +interface ErrorStoreData { + status: number; + request: Request; + data: ErrorResponseDto; + locale?: string; + rawError?: unknown; + known: boolean; +} + +type ErrorStore = Record; + +export const errorHandler = new Elysia() + .error({ + AppError, + }) + .state("error", {} as ErrorStore) + .state("startTime", {} as Record) + .use(ip()) + .onRequest(function setRequestMeta({ store }) { + store.startTime = {}; + store.error = {}; + const traceId = getTraceId() ?? ""; + if (traceId) { + store.startTime[traceId] = Bun.nanoseconds() / 1_000_000; } - - if (AUTH_INVALID_CODES.includes(bodyCode as AuthInvalidCode)) { - return response.betterAuthInvalidCred(status); + }) + .use(i18nMiddleware) + .onError({ as: "global" }, ({ code, error, set, store, request }) => { + const traceId = getTraceId() ?? ""; + if (traceId) { + set.headers["X-Trace-ID"] = traceId; } - return response.betterAuthGeneral(status, error); - } - - return response.internal(status); -}; + const setErrorResponse = (status: number, data: ErrorResponseDto) => { + store.error[traceId] = { + data, + status, + locale: store.locale[traceId], + request, + rawError: error, + known: true, + }; + }; + + if (AppError.isAppError(error)) { + setErrorResponse(error.statusCode, { + code: error.code as ErrorResponseDto["code"], + message: error.message, + }); + } else if (code === "NOT_FOUND") { + setErrorResponse(404, { + code: "NOT_FOUND", + message: "error.not-found", + }); + } else if (code === "VALIDATION") { + const detail = error.detail(error.message); + const message = typeof detail === "string" ? detail : detail.summary; + setErrorResponse(422, { + code: "VALIDATION_ERROR", + message, + }); + } else if (error instanceof BetterAuthAPIError) { + const bodyCode = error.body?.code || ""; + + if (AUTH_CONFLICT_CODES.includes(bodyCode as AuthConflictCode)) { + setErrorResponse(409, { + code: (error.body?.code || "ENTITY_CONFLICT") as ErrorResponseDto["code"], + message: error.body?.message, + }); + } else if (AUTH_INVALID_CODES.includes(bodyCode as AuthInvalidCode)) { + setErrorResponse(401, { + code: "INVALID_CREDENTIALS", + message: "error.invalid-cred", + }); + } else if (error.statusCode < 500) { + setErrorResponse(error.statusCode, { + code: "UNAUTHORIZED", + message: error.body?.message, + }); + } + } else if (error instanceof ElysiaCustomStatusResponse) { + if (error.code === 404) { + setErrorResponse(error.code, { + code: "NOT_FOUND", + message: "error.not-found", + }); + } + } else { + setErrorResponse(500, { + code: "INTERNAL_SERVER_ERROR", + message: "error.internal", + }); + store.error[traceId].known = false; + } + }) + .onError({ as: "global" }, function logErrorResponse({ store, status: statusFunc, ip }) { + const traceId = getTraceId() ?? ""; + const { request, locale, data, status, rawError, known } = store.error[traceId]; + const startTime = store.startTime[traceId]; + const requestPath = new URL(request.url).pathname; + + const userAgent = request.headers?.get("user-agent") ?? undefined; + + const logData = { + type: "http_request", + method: request.method, + path: requestPath, + status, + latency: startTime + ? `${(Bun.nanoseconds() / 1_000_000 - startTime).toFixed(3)}ms` + : undefined, + userAgent, + ip, + error: Bun.inspect(rawError), + errorCode: data.code, + }; + if (!known) { + appLogger[getLogLevelForRequest(status ?? 500)](logData.error, data); + } + pinoLogger[getLogLevelForRequest(status ?? 500)](formatGinLog(logData)); + return getErrorResponse(statusFunc, status, locale, data); + }); diff --git a/apps/backend/src/modules/auth/currentUser.ts b/apps/backend/src/handlers/auth/currentUser.ts similarity index 57% rename from apps/backend/src/modules/auth/currentUser.ts rename to apps/backend/src/handlers/auth/currentUser.ts index ebb8832..6768410 100644 --- a/apps/backend/src/modules/auth/currentUser.ts +++ b/apps/backend/src/handlers/auth/currentUser.ts @@ -4,18 +4,22 @@ import { betterAuthToCurrentUserInfoDto, ErrorResponseSchema, } from "@cvsa/core"; -import { authMiddleware } from "@common/middlewares"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( "/me", async ({ status, session }) => { - const userInfo = betterAuthToCurrentUserInfoDto(session.user); + const userInfo = await traceTask("getCurrentUser", async () => { + return betterAuthToCurrentUserInfoDto(session.user); + }); return status(200, userInfo); }, { detail: { summary: "Get current user", - description: "", + description: + "Retrieve the currently authenticated user's profile information. Requires a valid session cookie or a valid token in Authorization header.", }, response: { 200: CurrentUserInfoSchema, diff --git a/apps/backend/src/modules/auth/index.ts b/apps/backend/src/handlers/auth/index.ts similarity index 100% rename from apps/backend/src/modules/auth/index.ts rename to apps/backend/src/handlers/auth/index.ts diff --git a/apps/backend/src/modules/auth/login.ts b/apps/backend/src/handlers/auth/login.ts similarity index 71% rename from apps/backend/src/modules/auth/login.ts rename to apps/backend/src/handlers/auth/login.ts index e70f3d1..b78ad55 100644 --- a/apps/backend/src/modules/auth/login.ts +++ b/apps/backend/src/handlers/auth/login.ts @@ -12,6 +12,7 @@ import { toLoginResponse, toBetterAuthHeaders, } from "@cvsa/core"; +import { traceTask } from "@/common/trace"; const DAY = 86400; @@ -29,16 +30,18 @@ export const loginHandler = new Elysia() .post( "/session", async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await auth.api.signInEmail({ - body: { - email: body.email, - password: body.password, - }, - headers: toBetterAuthHeaders(headers), + const { user, token } = await traceTask("auth.signInEmail", async () => { + return await auth.api.signInEmail({ + body: { + email: body.email, + password: body.password, + }, + headers: toBetterAuthHeaders(headers), + }); }); if (!token) { - throw new AppError("Cannot login", "INTERNAL_SERVER_ERROR", 500, { + throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, { cause: "Better Auth responded with no token", }); } @@ -58,7 +61,8 @@ export const loginHandler = new Elysia() body: LoginRequestSchema, detail: { summary: "User Login", - description: "", + description: + "Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.", }, response: { 200: LoginResponseSchema, diff --git a/apps/backend/src/modules/auth/logout.ts b/apps/backend/src/handlers/auth/logout.ts similarity index 59% rename from apps/backend/src/modules/auth/logout.ts rename to apps/backend/src/handlers/auth/logout.ts index 35e4399..54bff43 100644 --- a/apps/backend/src/modules/auth/logout.ts +++ b/apps/backend/src/handlers/auth/logout.ts @@ -1,13 +1,16 @@ import { Elysia } from "elysia"; import { z } from "zod"; -import { authMiddleware } from "@common/middlewares"; +import { authMiddleware } from "@/middlewares"; import { auth, ErrorResponseSchema, toBetterAuthHeaders } from "@cvsa/core"; +import { traceTask } from "@/common/trace"; export const logoutHandler = new Elysia().use(authMiddleware).delete( "/session", async ({ set, headers, cookie: { token: tokenCookie } }) => { - await auth.api.signOut({ - headers: toBetterAuthHeaders(headers), + await traceTask("auth.signOut", async () => { + return await auth.api.signOut({ + headers: toBetterAuthHeaders(headers), + }); }); tokenCookie.remove(); @@ -18,7 +21,8 @@ export const logoutHandler = new Elysia().use(authMiddleware).delete( { detail: { summary: "User Logout", - description: "", + description: + "Terminate the current user session. Removes the authentication cookie and invalidates the session server-side.", }, response: { 204: z.null(), diff --git a/apps/backend/src/modules/auth/signup.ts b/apps/backend/src/handlers/auth/signup.ts similarity index 74% rename from apps/backend/src/modules/auth/signup.ts rename to apps/backend/src/handlers/auth/signup.ts index 119552a..7e8d05b 100644 --- a/apps/backend/src/modules/auth/signup.ts +++ b/apps/backend/src/handlers/auth/signup.ts @@ -13,6 +13,7 @@ import { import { RateLimitError } from "@common/error"; import { AppError } from "@cvsa/core"; import { auth } from "@cvsa/core"; +import { traceTask } from "@/common/trace"; const DAY = 86400; @@ -30,13 +31,15 @@ export const signupHandler = new Elysia() .post( "/user", async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await auth.api.signUpEmail({ - body: signupRequestToBetterAuth(body), - headers: toBetterAuthHeaders(headers), + const { user, token } = await traceTask("auth.signUpEmail", async () => { + return await auth.api.signUpEmail({ + body: signupRequestToBetterAuth(body), + headers: toBetterAuthHeaders(headers), + }); }); if (!token) { - throw new AppError("Cannot create user", "INTERNAL_SERVER_ERROR", 500, { + throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, { cause: "Better Auth responded with no token", }); } @@ -56,7 +59,8 @@ export const signupHandler = new Elysia() body: SignupRequestSchema, detail: { summary: "User Registration", - description: "", + description: + "Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.", }, response: { 200: SignupResponseSchema, diff --git a/apps/backend/src/modules/catalog/song/create.ts b/apps/backend/src/handlers/catalog/song/create.ts similarity index 62% rename from apps/backend/src/modules/catalog/song/create.ts rename to apps/backend/src/handlers/catalog/song/create.ts index fa2df95..23ca1fb 100644 --- a/apps/backend/src/modules/catalog/song/create.ts +++ b/apps/backend/src/handlers/catalog/song/create.ts @@ -1,19 +1,22 @@ import { Elysia } from "elysia"; import { CreateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; -import { authMiddleware } from "@common/middlewares/auth"; +import { authMiddleware } from "@/middlewares"; import { SongSchema } from "@cvsa/db"; +import { traceTask } from "@/common/trace"; export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( "/song", async ({ body, status }) => { - const song = await songService.create(body); + const song = await traceTask("songService.create", async () => { + return await songService.create(body); + }); return status(201, song); }, { body: CreateSongRequestSchema, detail: { summary: "Create Song", - description: "Create a new song (authentication required)", + description: "Create a new song entry in the catalog. Requires authentication.", }, response: { 201: SongSchema, diff --git a/apps/backend/src/modules/catalog/song/delete.ts b/apps/backend/src/handlers/catalog/song/delete.ts similarity index 53% rename from apps/backend/src/modules/catalog/song/delete.ts rename to apps/backend/src/handlers/catalog/song/delete.ts index 3ff1244..fe86ddc 100644 --- a/apps/backend/src/modules/catalog/song/delete.ts +++ b/apps/backend/src/handlers/catalog/song/delete.ts @@ -1,26 +1,28 @@ import { Elysia } from "elysia"; import { z } from "zod"; import { ErrorResponseSchema, songService } from "@cvsa/core"; -import { AppError } from "@cvsa/core"; -import { authMiddleware } from "@common/middlewares/auth"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) .use(authMiddleware) .delete( "/song/:id", async ({ params, set }) => { - const id = Number(params.id); - if (Number.isNaN(id)) { - throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); - } - await songService.delete(id); + await traceTask("songService.delete", async () => { + return await songService.delete(params.id); + }); set.status = 204; return null; }, { + params: z.object({ + id: z.coerce.number().int().positive(), + }), detail: { summary: "Delete Song", - description: "Soft delete a song (authentication required)", + description: + "Soft delete a song from the catalog. Requires authentication. The song record is marked as deleted but retained in the database for data integrity.", }, response: { 204: z.null(), diff --git a/apps/backend/src/modules/catalog/song/details.ts b/apps/backend/src/handlers/catalog/song/details.ts similarity index 60% rename from apps/backend/src/modules/catalog/song/details.ts rename to apps/backend/src/handlers/catalog/song/details.ts index 5143c5e..7fe95f2 100644 --- a/apps/backend/src/modules/catalog/song/details.ts +++ b/apps/backend/src/handlers/catalog/song/details.ts @@ -1,17 +1,21 @@ import { Elysia } from "elysia"; import { ErrorResponseSchema, SongDetailsResponseSchema, songService } from "@cvsa/core"; import z from "zod"; +import { traceTask } from "@/common/trace"; export const songDetailsHandler = new Elysia().get( "/song/:id/details", async ({ params, status }) => { - const song = await songService.getDetails(params.id); + const song = await traceTask("songService.getDetails", async () => { + return await songService.getDetails(params.id); + }); return status(200, song); }, { detail: { summary: "Song Details", - description: "", + description: + "Retrieve detailed information about a specific song by its ID. Includes all metadata such as title, artists, album, duration, and audio sources.", }, response: { 200: SongDetailsResponseSchema, diff --git a/apps/backend/src/modules/catalog/song/index.ts b/apps/backend/src/handlers/catalog/song/index.ts similarity index 100% rename from apps/backend/src/modules/catalog/song/index.ts rename to apps/backend/src/handlers/catalog/song/index.ts diff --git a/apps/backend/src/modules/catalog/song/search.ts b/apps/backend/src/handlers/catalog/song/search.ts similarity index 54% rename from apps/backend/src/modules/catalog/song/search.ts rename to apps/backend/src/handlers/catalog/song/search.ts index 8f0f8a8..729a059 100644 --- a/apps/backend/src/modules/catalog/song/search.ts +++ b/apps/backend/src/handlers/catalog/song/search.ts @@ -1,18 +1,22 @@ import { Elysia } from "elysia"; import { songSearchService } from "@cvsa/core"; import z from "zod"; +import { traceTask } from "@/common/trace"; // TODO: add corresponding DTO and response schema export const songSearchHandler = new Elysia().get( "/songs", async ({ query, status }) => { - const song = await songSearchService.search(query.q || ""); + const song = await traceTask("songSearchService.search", async () => { + return await songSearchService.search(query.q || ""); + }); return status(200, song); }, { detail: { summary: "Search for Songs", - description: "", + description: + "Full-text search across song titles, artist names, and album titles. Returns a list of matching songs ordered by relevance.", }, query: z.object({ q: z.string().optional(), diff --git a/apps/backend/src/modules/catalog/song/update.ts b/apps/backend/src/handlers/catalog/song/update.ts similarity index 57% rename from apps/backend/src/modules/catalog/song/update.ts rename to apps/backend/src/handlers/catalog/song/update.ts index 643130f..0aa6bcb 100644 --- a/apps/backend/src/modules/catalog/song/update.ts +++ b/apps/backend/src/handlers/catalog/song/update.ts @@ -1,26 +1,29 @@ import { Elysia } from "elysia"; +import { z } from "zod"; import { UpdateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; -import { AppError } from "@cvsa/core"; -import { authMiddleware } from "@common/middlewares/auth"; +import { authMiddleware } from "@/middlewares"; import { SongSchema } from "@cvsa/db"; +import { traceTask } from "@/common/trace"; export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }) .use(authMiddleware) .patch( "/song/:id", async ({ params, body, status }) => { - const id = Number(params.id); - if (Number.isNaN(id)) { - throw new AppError("Invalid song ID", "VALIDATION_ERROR", 400); - } - const song = await songService.update(id, body); + const song = await traceTask("songService.update", async () => { + return await songService.update(params.id, body); + }); return status(200, song); }, { body: UpdateSongRequestSchema, + params: z.object({ + id: z.coerce.number().int().positive(), + }), detail: { summary: "Update Song", - description: "Update an existing song (authentication required)", + description: + "Update metadata of an existing song identified by its ID. Requires authentication.", }, response: { 200: SongSchema, diff --git a/apps/backend/src/handlers/dev/gc.ts b/apps/backend/src/handlers/dev/gc.ts new file mode 100644 index 0000000..207b9d6 --- /dev/null +++ b/apps/backend/src/handlers/dev/gc.ts @@ -0,0 +1,12 @@ +import { Elysia } from "elysia"; +import { env } from "@cvsa/env"; + +export const gcHandler = new Elysia().get("/gc-result", async ({ status }) => { + if (env.NODE_ENV !== "development") { + throw status(404); + } + Bun.gc(); + return status(200, { + success: true, + }); +}); diff --git a/apps/backend/src/handlers/dev/index.ts b/apps/backend/src/handlers/dev/index.ts new file mode 100644 index 0000000..658936f --- /dev/null +++ b/apps/backend/src/handlers/dev/index.ts @@ -0,0 +1,7 @@ +import { Elysia } from "elysia"; +import { getHeapSnapshotHandler } from "./snapshot"; +import { gcHandler } from "./gc"; + +export const devHandler = new Elysia({ name: "devHandler" }) + .use(getHeapSnapshotHandler) + .use(gcHandler); diff --git a/apps/backend/src/handlers/dev/snapshot.ts b/apps/backend/src/handlers/dev/snapshot.ts new file mode 100644 index 0000000..e5c936b --- /dev/null +++ b/apps/backend/src/handlers/dev/snapshot.ts @@ -0,0 +1,13 @@ +import { Elysia } from "elysia"; +import { env } from "@cvsa/env"; +import v8 from "node:v8"; + +export const getHeapSnapshotHandler = new Elysia().get("/heap-snapshot", async ({ status }) => { + if (env.NODE_ENV !== "development") { + throw status(404); + } + const snapshotPath = v8.writeHeapSnapshot(); + return status(200, { + path: snapshotPath, + }); +}); diff --git a/apps/backend/src/modules/index.ts b/apps/backend/src/handlers/index.ts similarity index 70% rename from apps/backend/src/modules/index.ts rename to apps/backend/src/handlers/index.ts index e059185..190d46f 100644 --- a/apps/backend/src/modules/index.ts +++ b/apps/backend/src/handlers/index.ts @@ -1,2 +1,3 @@ export * from "./auth"; export * from "./catalog/song"; +export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 8bca8b4..ecdb0e6 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,12 +2,17 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler, songHandler } from "@modules/index"; -import { AppError } from "@cvsa/core"; +import { authHandler, songHandler } from "@handlers/index"; import { errorHandler } from "./errorHandler"; import { openapi } from "@elysiajs/openapi"; +import { requestLoggerMiddleware } from "@/middlewares"; +import { opentelemetry } from "@elysiajs/opentelemetry"; +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { devHandler } from "./handlers"; const [host, port] = getBindingInfo(); + logStartup(host, port); export const app = new Elysia({ @@ -16,14 +21,18 @@ export const app = new Elysia({ }, prefix: "/v2", }) - .error({ - AppError, - }) - .onError(errorHandler) + .use( + opentelemetry({ + spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())], + }) + ) + .use(onAfterHandler) + .use(requestLoggerMiddleware) + .use(errorHandler) .use(openapi()) .use(authHandler) .use(songHandler) - .use(onAfterHandler) + .use(devHandler) .listen(16412); export const VERSION = pkg.version; diff --git a/apps/backend/src/common/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts similarity index 87% rename from apps/backend/src/common/middlewares/auth.ts rename to apps/backend/src/middlewares/auth.ts index 2c7cd51..ec714de 100644 --- a/apps/backend/src/common/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -9,7 +9,7 @@ export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( }); if (!session) { - throw new AppError("Unauthorized", "UNAUTHORIZED", 401); + throw new AppError("error.unauthorized", "UNAUTHORIZED", 401); } return { diff --git a/apps/backend/src/middlewares/i18n.ts b/apps/backend/src/middlewares/i18n.ts new file mode 100644 index 0000000..45188bc --- /dev/null +++ b/apps/backend/src/middlewares/i18n.ts @@ -0,0 +1,42 @@ +import { getTraceId } from "@/common/trace"; +import { Elysia } from "elysia"; + +const getLocale = (headers: Record) => { + if (headers["x-locale"]) { + return { + locale: headers["x-locale"], + }; + } + const acceptLang = headers["accept-language"]; + if (acceptLang) { + const langList = acceptLang.split(","); + for (const lang of langList) { + if (lang.includes("zh")) { + return { + locale: "zh", + }; + } else if (lang.includes("en")) { + return { + locale: "en", + }; + } + } + } + return { + locale: "en", + }; +}; + +export const i18nMiddleware = new Elysia({ name: "i18nMiddleware" }) + .state("locale", {} as Record) + .onError({ as: "global" }, ({ request, store }) => { + const traceId = getTraceId(); + const { locale } = getLocale(Object.fromEntries(request.headers.entries())); + if (traceId) { + store.locale = {}; + store.locale[traceId] = locale; + } + }) + .derive({ as: "scoped" }, ({ headers }) => { + return getLocale(headers); + }); diff --git a/apps/backend/src/middlewares/index.ts b/apps/backend/src/middlewares/index.ts new file mode 100644 index 0000000..cb3c85a --- /dev/null +++ b/apps/backend/src/middlewares/index.ts @@ -0,0 +1,3 @@ +export * from "./auth"; +export * from "./requestLogger"; +export * from "./i18n"; diff --git a/apps/backend/src/middlewares/requestLogger.ts b/apps/backend/src/middlewares/requestLogger.ts new file mode 100644 index 0000000..0286544 --- /dev/null +++ b/apps/backend/src/middlewares/requestLogger.ts @@ -0,0 +1,56 @@ +import Elysia from "elysia"; +import type { Logger } from "pino"; +import { pinoLogger } from "@cvsa/logger"; +import { ip } from "elysia-ip"; +import { formatGinLog, getLogLevelForRequest } from "@common/log"; + +export interface RequestLoggerOptions { + excludePaths?: string[]; +} + +export function createRequestLoggerMiddleware(pinoLogger: Logger) { + return new Elysia() + .use(ip()) + .derive(function setRequestMeta() { + return { + startTime: Bun.nanoseconds() / 1_000_000, + }; + }) + .onAfterHandle({ as: "global" }, function afterResponseLog(context) { + const { request, ip, responseValue, startTime } = context; + const requestPath = new URL(request.url).pathname; + + const status = (() => { + if (!responseValue) { + return undefined; + } + const r = responseValue as { status?: number }; + if (r.status) { + return r.status; + } + const r2 = responseValue as { code?: number }; + if (r2.code) { + return r2.code; + } + return undefined; + })(); + + const userAgent = request.headers?.get("user-agent") ?? undefined; + + const logData: Record = { + type: "http_request", + method: request.method, + path: requestPath, + status, + latency: startTime + ? `${(Bun.nanoseconds() / 1_000_000 - startTime).toFixed(3)}ms` + : undefined, + userAgent, + ip, + }; + + pinoLogger[getLogLevelForRequest(status ?? 200)](formatGinLog(logData)); + }); +} + +export const requestLoggerMiddleware = createRequestLoggerMiddleware(pinoLogger); diff --git a/apps/backend/src/onAfterHandle.ts b/apps/backend/src/onAfterHandle.ts index 681d8bc..39782a3 100644 --- a/apps/backend/src/onAfterHandle.ts +++ b/apps/backend/src/onAfterHandle.ts @@ -1,10 +1,16 @@ import Elysia, { ElysiaFile } from "elysia"; +import { getTraceId } from "@/common/trace"; const encoder = new TextEncoder(); -export const onAfterHandler = new Elysia().onAfterHandle( - { as: "global" }, - ({ responseValue, request }) => { +export const onAfterHandler = new Elysia() + .onBeforeHandle({ as: "global" }, ({ set }) => { + const traceId = getTraceId(); + if (traceId) { + set.headers["X-Trace-ID"] = traceId; + } + }) + .onAfterHandle({ as: "global" }, ({ responseValue, request }) => { const contentType = request.headers.get("Content-Type") || ""; const accept = request.headers.get("Accept") || ""; const secFetchMode = request.headers.get("Sec-Fetch-Mode"); @@ -18,11 +24,13 @@ export const onAfterHandler = new Elysia().onAfterHandle( if (responseValue instanceof ElysiaFile || responseValue instanceof Response) { return; } + if (responseValue === null) { + return; + } const realResponse = responseValue as { code?: number; response?: unknown }; - if (realResponse.code) { - const text = isBrowser - ? JSON.stringify(realResponse.response, null, 2) - : JSON.stringify(realResponse.response); + + if (realResponse.code && isBrowser) { + const text = JSON.stringify(realResponse.response, null, 4); return new Response(encoder.encode(text), { headers: { "Content-Type": "application/json; charset=utf-8", @@ -30,13 +38,13 @@ export const onAfterHandler = new Elysia().onAfterHandle( status: realResponse.code, }); } - const text = isBrowser - ? JSON.stringify(realResponse, null, 2) - : JSON.stringify(realResponse); - return new Response(encoder.encode(text), { - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); - } -); + if (isBrowser) { + const text = JSON.stringify(realResponse, null, 4); + + return new Response(encoder.encode(text), { + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + } + }); diff --git a/apps/backend/src/startMessage.ts b/apps/backend/src/startMessage.ts index b57fb83..7a5b5e6 100644 --- a/apps/backend/src/startMessage.ts +++ b/apps/backend/src/startMessage.ts @@ -1,4 +1,4 @@ -import { env } from "@cvsa/core"; +import { env } from "@cvsa/env"; import chalk from "chalk"; import os from "node:os"; diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index ba562f0..8fdfa40 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@cvsa/core"; +import { prisma } from "@cvsa/db"; const api = treaty(app); diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts index 634cb7b..f80c232 100644 --- a/apps/backend/tests/song.test.ts +++ b/apps/backend/tests/song.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@cvsa/core"; +import { prisma } from "@cvsa/db"; const api = treaty(app); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 369cbec..e85b148 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "paths": { "@/*": ["./src/*"], - "@modules/*": ["./src/modules/*"], + "@handlers/*": ["./src/handlers/*"], "@common/*": ["./src/common/*"] } }, diff --git a/bun.lock b/bun.lock index 78b40d6..9f1809c 100644 --- a/bun.lock +++ b/bun.lock @@ -13,13 +13,20 @@ }, "apps/backend": { "name": "@cvsa/backend", - "version": "0.0.1", + "version": "2.1.0", "dependencies": { "@better-auth/prisma-adapter": "^1.5.4", "@cvsa/core": "workspace:*", "@cvsa/db": "workspace:*", - "@elysiajs/eden": "^1.0.5", + "@cvsa/env": "workspace:*", + "@cvsa/logger": "workspace:*", + "@elysiajs/eden": "^1.4.9", "@elysiajs/openapi": "^1.4.14", + "@elysiajs/opentelemetry": "^1.4.10", + "@fanee/runtime-node": "^0.2.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", + "@opentelemetry/sdk-trace-node": "^2.6.1", "@prisma/adapter-pg": "^7.4.1", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", @@ -29,6 +36,7 @@ "elysia-ip": "^1.0.10", "elysia-rate-limit": "^4.5.0", "nanoid": "^5.1.6", + "pino": "^10.3.1", "zod": "^4.3.6", }, "devDependencies": { @@ -39,17 +47,19 @@ }, "packages/core": { "name": "@cvsa/core", + "version": "0.1.0", "dependencies": { - "@better-auth/prisma-adapter": "^1.5.5", - "@cvsa/db": "*", - "@huggingface/hub": "^2.11.0", - "@huggingface/transformers": "^4.0.0", - "@t3-oss/env-core": "^0.13.10", - "better-auth": "^1.5.5", + "@better-auth/prisma-adapter": "^1.5.6", + "@cvsa/db": "workspace:*", + "@cvsa/embedding": "workspace:*", + "@cvsa/env": "workspace:*", + "@cvsa/logger": "workspace:*", + "@cvsa/observability": "workspace:*", + "@elysiajs/eden": "^1.4.9", + "better-auth": "^1.5.6", "meilisearch": "^0.56.0", - "nanoid": "^5.1.6", - "onnxruntime-node": "^1.24.3", - "remeda": "^2.33.6", + "nanoid": "^5.1.7", + "remeda": "^2.33.7", "zod": "^4.3.6", }, "devDependencies": { @@ -61,6 +71,7 @@ "packages/db": { "name": "@cvsa/db", "dependencies": { + "@cvsa/env": "workspace:*", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "@types/pg": "^8.16.0", @@ -73,6 +84,72 @@ "prisma-json-types-generator": "^4.1.1", }, }, + "packages/embedding": { + "name": "@cvsa/embedding", + "dependencies": { + "@cvsa/logger": "workspace:*", + "@elysiajs/eden": "^1.4.9", + "@huggingface/hub": "^2.11.0", + "@huggingface/transformers": "^4.0.1", + "elysia": "latest", + "onnxruntime-node": "^1.24.3", + "zod": "^4.3.6", + }, + "devDependencies": { + "@cvsa/typescript-config": "workspace:*", + "bun-types": "latest", + }, + }, + "packages/env": { + "name": "@cvsa/env", + "version": "0.1.0", + "dependencies": { + "@t3-oss/env-core": "^0.13.11", + "zod": "^4.3.6", + }, + "devDependencies": { + "@cvsa/typescript-config": "workspace:*", + "@types/bun": "latest", + }, + }, + "packages/logger": { + "name": "@cvsa/logger", + "version": "0.1.0", + "dependencies": { + "@cvsa/env": "workspace:*", + "@cvsa/observability": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-logs": "^0.214.0", + "@opentelemetry/sdk-node": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@t3-oss/env-core": "^0.13.11", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "zod": "^4.3.6", + }, + "devDependencies": { + "@cvsa/typescript-config": "workspace:*", + "@types/bun": "latest", + }, + }, + "packages/observability": { + "name": "@cvsa/observability", + "version": "0.1.0", + "dependencies": { + "@cvsa/env": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-node": "^0.214.0", + }, + "devDependencies": { + "@cvsa/typescript-config": "workspace:*", + "@types/bun": "latest", + }, + }, "packages/typescript-config": { "name": "@cvsa/typescript-config", }, @@ -80,19 +157,19 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core": ["@better-auth/core@1.5.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0" } }, "sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0" } }, "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.6", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.6" } }, "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ=="], "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], @@ -128,6 +205,14 @@ "@cvsa/db": ["@cvsa/db@workspace:packages/db"], + "@cvsa/embedding": ["@cvsa/embedding@workspace:packages/embedding"], + + "@cvsa/env": ["@cvsa/env@workspace:packages/env"], + + "@cvsa/logger": ["@cvsa/logger@workspace:packages/logger"], + + "@cvsa/observability": ["@cvsa/observability@workspace:packages/observability"], + "@cvsa/prisma-zod": ["@cvsa/prisma-zod@3.6.0", "", { "dependencies": { "@prisma/generator-helper": "^7.5.0" }, "peerDependencies": { "@prisma/client": ">= 7.0.0", "prisma": ">= 7.0.0", "zod": ">= 4.0.0" }, "bin": { "prisma-zod": "dist/index.js" } }, "sha512-5/Vh93EqlaXwwJk9cudIclmZG8fX4XsQ2LiGSSN5JcszOzm/MPXuyuGJU9M70X1P+KAB8Vza8RsvXdXmTTJbSw=="], "@cvsa/typescript-config": ["@cvsa/typescript-config@workspace:packages/typescript-config"], @@ -138,14 +223,22 @@ "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="], - "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], "@elysiajs/openapi": ["@elysiajs/openapi@1.4.14", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-kWmJWdvP8/LwHwAJXSpz6xFfYUoyUyEPRimEYABuDU1rOnS27Da1u9T2jyU7frOopxKWV/wDfDxMP8z2xdCPJw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], + "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.11", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-GGggB40rXithCA9nSjSc25TFkBPeEEW3rD8me/TrRq4TejzWFJn5zEKdRMYWSpYBeiUsraCpQxu94K2ASz+UnA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], + "@fanee/runtime-node": ["@fanee/runtime-node@0.2.1", "", { "dependencies": { "messageformat": "^4.0.0" } }, "sha512-txiKblLxjsO49SYeZDb9CClBp3EC7lp0kn7Zfm1CDT47nbrxIvUqupH/DOQbE0gjPIyv8MOkXZmjmBTORvxd7w=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@huggingface/hub": ["@huggingface/hub@2.11.0", "", { "dependencies": { "@huggingface/tasks": "^0.19.90" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ=="], @@ -156,7 +249,7 @@ "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], - "@huggingface/transformers": ["@huggingface/transformers@4.0.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", "sharp": "^0.34.5" } }, "sha512-7shdBRvtjIDP7UUhdi0b4CMRCD+QXvQIsEsxECWS9VEDEsxSQF9Ci+51mwy2OWI8vbsoic8e4dbR4eI8aZWxaw=="], + "@huggingface/transformers": ["@huggingface/transformers@4.0.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", "sharp": "^0.34.5" } }, "sha512-tAQYEy+cnW0ku/NxBSjFXCymi+DZa1/JkoGf4McxjzO36CZZIL/J4TF6X7i/tzs75yTjshUDgsvSz03s2xym2A=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -208,16 +301,76 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.6", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/configuration": ["@opentelemetry/configuration@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-Q+awuEwxhETwIAXuxHvIY5ZMEP0ZqvxLTi9kclrkyVJppEUXYL3Bhiw3jYrxdHYMh0Y0tVInQH9FEZ1aMinvLA=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IWAVvCO1TlpotRjFmhQFz9RSfQy5BsLtDRBtptSrXZRwfyRPpuql/RMe5zdmu0Gxl3ERDFwOzOqkf3bwy7Jzcw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-0NGxWHVYHgbp51SEzmsP+Hdups81eRs229STcSWHo3WO0aqY6RpJ9csxfyEtFgaNrBDv6UfOh0je4ss/ROS6XA=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pJIcghFGhx3VSCgP5U+yZx+OMNj0t+ttnhC8IjL5Wza7vWIczctF6t3AGcVQffi2dEqX+ZHANoBwoPR8y6RMKA=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4TGYoZKebUWVuYkV6r5wS2dUF4zH7EbWFw/Uqz1ZM1tGHQeFT9wzHGXq3iSIXMUrwu5jRdxjfMaXrYejPu2kpQ=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FWRZ7AWoTryYhthralHkfXUuyO3l7cRsnr49WcDio1orl2a7KxT8aDZdwQtV1adzoUvZ9Gfo+IstElghCS4zfw=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-km2/hD3inLTqtLnUAHDGz7ZP/VOyZNslrC/iN66x4jkmpckwlONW54LRPNI6fm09/musDtZga9EWsxgwnjGUlw=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dvz9TA6cPqIbxolSzQ5x9br6iQlqdGhVYrm+oYc7pfJ7LaVXz8F0XIqhWbnKB5YvfZ6SUmabBUUxnvHs/9uhxA=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-kKFMxBcjBZAC1vBch5mtZ/dJQvcAEKWga+c+q5iGgRLPIE6Mc649zEwMaCIQCzalziMJQiyUadFYMHmELB7AFw=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/configuration": "0.214.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.214.0", "@opentelemetry/exporter-logs-otlp-http": "0.214.0", "@opentelemetry/exporter-logs-otlp-proto": "0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.214.0", "@opentelemetry/exporter-prometheus": "0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "0.214.0", "@opentelemetry/exporter-zipkin": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/propagator-b3": "2.6.1", "@opentelemetry/propagator-jaeger": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-gl2XvQBJuPjhGcw9SsnQO5qxChAPMuGRPFaD8lqtF+Cey91NgGUQ0sio2vlDFOSm3JOLzc44vL+OAfx1dXuZjg=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="], "@prisma/client": ["@prisma/client@7.6.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.6.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ=="], @@ -230,7 +383,7 @@ "@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="], - "@prisma/dmmf": ["@prisma/dmmf@7.5.0", "", {}, "sha512-MAIM8Uji/UuT6PokBwfOHtLHV2GTPpeAwcaFDqNvSZR+rbxrV/gJeLuOjmSnFkGpro/Y0un4oxAkUO7mGQeY6g=="], + "@prisma/dmmf": ["@prisma/dmmf@7.6.0", "", {}, "sha512-OZFEOEziUnJJMx2vu3twM5jEIsEyCRDPXq2Y/2XMBNPhnscKvbDWS3DAYyWmH8jqkDn4VqyE1UCvEFt30Pqbww=="], "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], @@ -240,9 +393,9 @@ "@prisma/fetch-engine": ["@prisma/fetch-engine@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/get-platform": "7.6.0" } }, "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w=="], - "@prisma/generator": ["@prisma/generator@7.5.0", "", {}, "sha512-GwXeQQ3PFN6EPLCXN8lNjQfC1ssE7wm8AMm7kR8Xbi00REiBn7rB4G5QieW1Nv7zl9rqrpAfG5gfOUU0GAaA+w=="], + "@prisma/generator": ["@prisma/generator@7.6.0", "", {}, "sha512-F09ZN/QVlKuvoAKEnHZB3LBbooKQTrio7NrKhI7tf0C9XZuCoALP/F7gADpTayXDmPaII1Klit3SJmJsfantEw=="], - "@prisma/generator-helper": ["@prisma/generator-helper@7.5.0", "", { "dependencies": { "@prisma/debug": "7.5.0", "@prisma/dmmf": "7.5.0", "@prisma/generator": "7.5.0" } }, "sha512-qxV/cqoGyKpt9sKlH5k2FoMlpo7gr/dnb2QvCfOaeWYEFanKQLjRE4lr5MovCq3ihfW5oeOpwuiSqz5SG5nlJg=="], + "@prisma/generator-helper": ["@prisma/generator-helper@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/dmmf": "7.6.0", "@prisma/generator": "7.6.0" } }, "sha512-Wnb7NypN2581AXsaDdfGM6Guvfg6ahs0ooWHhFwD+/mrE3rI122cNRWMV9W1hqyt5OITs+3TV2M6GZZ16NcwjQ=="], "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], @@ -288,51 +441,57 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.11", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-d1zTcIf6VWT7cdfjhi0X36C2PRsUi2HdEwYzVgkLHmuuYtL+1Y1Zu3JdlouoB/NjG2vX3q4NnKLMNhDOEweoIg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AwJ4mA++Kpem33Lcov093hS1LrgqbKxqq5FCReoqsA8ayEG6eAJAo8ItDd9qQTdBiXxZH8GHCspLAMIe1t3Xyw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HT9SjKkjEw9uvlgly/qwCGEm4wOXOwQPSPS+wkg+/O1Qan3F1uU/0PFYzxl3m4lfuV3CP9wr2Dq5dPrUX+B9Ag=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+4s5GZs3kjxc1KMhLBhoQy4UBkXjOhgidA9ipNllkA4JLivSqUCuOgU1Xbyp6vzYrsqHJ9vvwo/2mXgEtD6ZHg=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ZO7GCyQd5HV564XWHc9KysjanFfM3DmnWquyEByu+hQMq42g9OMU/fYOCfHS6Xj2aXkIg2FHJeRV+iAck2YrbQ=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-BjX2fdz38mBb/H94JXrD5cJ+mEq8NmsCbYdC42JzQebJ0X8EdNgyFoEhOydPGViOmaRmhhdZnPZKKn6wahSpcA=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], - "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], + "better-auth": ["better-auth@1.5.6", "", { "dependencies": { "@better-auth/core": "1.5.6", "@better-auth/drizzle-adapter": "1.5.6", "@better-auth/kysely-adapter": "1.5.6", "@better-auth/memory-adapter": "1.5.6", "@better-auth/mongo-adapter": "1.5.6", "@better-auth/prisma-adapter": "1.5.6", "@better-auth/telemetry": "1.5.6", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.12", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ=="], "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], @@ -340,8 +499,6 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -354,8 +511,18 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -368,6 +535,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], @@ -376,7 +545,7 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -400,6 +569,8 @@ "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -408,6 +579,8 @@ "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], @@ -416,20 +589,28 @@ "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "file-type": ["file-type@21.3.3", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-pNwbwz8c3aZ+GvbJnIsCnDjKvgCZLHxkFWLEFxU3RMa+Ey++ZSEfisvsWQMcdys6PpxQjWUOIDi1fifXsW3YRg=="], + "file-type": ["file-type@22.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw=="], "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], @@ -450,7 +631,11 @@ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -458,6 +643,10 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], @@ -468,11 +657,15 @@ "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "kysely": ["kysely@0.28.14", "", {}, "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA=="], + "kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -484,11 +677,11 @@ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], - "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + "messageformat": ["messageformat@4.0.0", "", {}, "sha512-XKmJ/ffTWToWOlHJzt85ZChQgVGC0LHzNWuNK8zuYpNySsB0nIEmytOdSAOW9ETKtkajAUJf520m5gFHHnrTYg=="], - "mongodb": ["mongodb@7.1.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], @@ -500,7 +693,7 @@ "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], - "node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], + "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], @@ -512,6 +705,10 @@ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="], "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], @@ -522,6 +719,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -544,6 +743,14 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="], + + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], @@ -562,14 +769,18 @@ "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], @@ -578,20 +789,32 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "remeda": ["remeda@2.33.6", "", {}, "sha512-tazDGH7s75kUPGBKLvhgBEHMgW+TdDFhjUAMdQj57IoWz6HsGa5D2RX5yDUz6IIqiRRvZiaEHzCzWdTeixc/Kg=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "remeda": ["remeda@2.33.7", "", {}, "sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -608,11 +831,13 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -626,19 +851,23 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - "try": ["try@1.0.3", "", {}, "sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.9.1", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.1", "@turbo/darwin-arm64": "2.9.1", "@turbo/linux-64": "2.9.1", "@turbo/linux-arm64": "2.9.1", "@turbo/windows-64": "2.9.1", "@turbo/windows-arm64": "2.9.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-TO9du8MwLTAKoXcGezekh9cPJabJUb0+8KxtpMR6kXdRASrmJ8qXf2GkVbCREgzbMQakzfNcux9cZtxheDY4RQ=="], + "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="], "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], @@ -650,31 +879,43 @@ "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@elysiajs/opentelemetry/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], + + "@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], + "@prisma/dev/remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], - "@prisma/generator-helper/@prisma/debug": ["@prisma/debug@7.5.0", "", {}, "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg=="], - "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], @@ -682,6 +923,104 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@3.0.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], + + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], } } diff --git a/locale/manifest.json b/locale/manifest.json new file mode 100644 index 0000000..206de88 --- /dev/null +++ b/locale/manifest.json @@ -0,0 +1,4 @@ +{ + "format": "otb", + "specVersion": "0.3.0" +} diff --git a/locale/modules/backend/manifest.json b/locale/modules/backend/manifest.json new file mode 100644 index 0000000..206de88 --- /dev/null +++ b/locale/modules/backend/manifest.json @@ -0,0 +1,4 @@ +{ + "format": "otb", + "specVersion": "0.3.0" +} diff --git a/locale/modules/backend/messages/en.json b/locale/modules/backend/messages/en.json new file mode 100644 index 0000000..31612ea --- /dev/null +++ b/locale/modules/backend/messages/en.json @@ -0,0 +1,9 @@ +{ + "error.unauthorized": "Unauthorized", + "error.song.notfound": "Song Not Found", + "error.signup.failed": "Failed to Sign Up", + "error.login.failed": "Failed to Log In", + "error.not-found": "Not Found", + "error.invalid-cred": "Invalid Credentials", + "error.internal": "Internal Error" +} diff --git a/locale/modules/backend/messages/zh.json b/locale/modules/backend/messages/zh.json new file mode 100644 index 0000000..ef0ec63 --- /dev/null +++ b/locale/modules/backend/messages/zh.json @@ -0,0 +1,9 @@ +{ + "error.unauthorized": "未登录", + "error.song.notfound": "歌曲不存在", + "error.signup.failed": "无法注册", + "error.login.failed": "无法登录", + "error.not-found": "请求的路径不存在", + "error.invalid-cred": "提交的凭据无效", + "error.internal": "服务器内部错误" +} diff --git a/packages/core/common/index.ts b/packages/core/common/index.ts deleted file mode 100644 index 952334b..0000000 --- a/packages/core/common/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./schema"; -export * from "./error"; -export * from "./prisma"; -export * from "./env"; diff --git a/packages/core/common/internal.ts b/packages/core/common/internal.ts deleted file mode 100644 index a05624b..0000000 --- a/packages/core/common/internal.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Internal entry point for the core package. - * * Provides access to internal members and objects from `@cvsa/core/common` - * that are intended for internal use only and are not part of the public API. - */ - -export * from "./env"; -export * from "./utils"; -export * from "./prisma"; -export * from "./types"; diff --git a/packages/core/common/schema/index.ts b/packages/core/common/schema/index.ts deleted file mode 100644 index 0ad13d9..0000000 --- a/packages/core/common/schema/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./error"; diff --git a/packages/core/common/types/repository.ts b/packages/core/common/types/repository.ts deleted file mode 100644 index dd2f09b..0000000 --- a/packages/core/common/types/repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { TxClient } from "./TxClient"; - -export interface IDetailsRepository { - getDetailsById(id: number | string, tx?: TxClient): Promise; -} diff --git a/packages/core/index.ts b/packages/core/index.ts deleted file mode 100644 index c0e36b9..0000000 --- a/packages/core/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./common"; -export * from "./modules"; diff --git a/packages/core/modules/catalog/song/service.ts b/packages/core/modules/catalog/song/service.ts deleted file mode 100644 index 5da0f71..0000000 --- a/packages/core/modules/catalog/song/service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { AppError, type SongSearchService } from "@cvsa/core"; -import type { Song } from "@cvsa/db"; -import type { - SongDetailsResponseDto, - SongId, - CreateSongRequestDto, - UpdateSongRequestDto, -} from "./dto"; -import type { ISongRepository } from "./repository.interface"; -import type { IDetailsService, Serialized } from "@cvsa/core/common"; - -export class SongService implements IDetailsService { - constructor( - private readonly repository: ISongRepository, - private readonly search: SongSearchService - ) {} - - async getDetails(id: SongId) { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("Song not found", "NOT_FOUND", 404); - } - return result; - } - - async create(input: CreateSongRequestDto): Promise> { - const result = await this.repository.create(input); - try { - this.search.sync(result.id); - } catch (e) { - console.error(e); - } - return result; - } - - async update(id: SongId, input: UpdateSongRequestDto): Promise> { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("Song not found", "NOT_FOUND", 404); - } - const result = await this.repository.update(id, input); - try { - this.search.sync(id); - } catch (e) { - console.error(e); - } - return result; - } - - async delete(id: SongId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("Song not found", "NOT_FOUND", 404); - } - await this.repository.softDelete(id); - try { - this.search.sync(id); - } catch (e) { - console.error(e); - } - } -} diff --git a/packages/core/modules/internal.ts b/packages/core/modules/internal.ts deleted file mode 100644 index 422fc78..0000000 --- a/packages/core/modules/internal.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./catalog"; -export * from "./search"; -export * from "./ml"; diff --git a/packages/core/modules/ml/index.ts b/packages/core/modules/ml/index.ts deleted file mode 100644 index 5b00d36..0000000 --- a/packages/core/modules/ml/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./embedding"; diff --git a/packages/core/modules/search/search.interface.ts b/packages/core/modules/search/search.interface.ts deleted file mode 100644 index 6d164c0..0000000 --- a/packages/core/modules/search/search.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IDetailsRepository } from "@cvsa/core/common"; -import type { SearchManager } from "./manager"; -import type { SearchResponse } from "meilisearch"; -import type { EmbeddingManager } from "@cvsa/core/modules"; - -export abstract class ISearchService { - constructor( - protected readonly repository: IDetailsRepository, - protected readonly manager?: SearchManager, - protected readonly embeddingManager?: EmbeddingManager - ) {} - abstract sync(id: number): Promise; - abstract search(query: string, language: string): Promise; -} diff --git a/packages/core/package.json b/packages/core/package.json index c14787c..6a8ce0c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,9 @@ { "name": "@cvsa/core", "type": "module", - "module": "index.ts", + "main": "src/index.ts", + "module": "src/index.ts", + "version": "0.1.0", "scripts": { "lint": "bunx --bun biome lint", "test": "bun test", @@ -16,21 +18,21 @@ "@cvsa/typescript-config": "workspace:*" }, "exports": { - ".": "./index.ts", - "./common": "./common/internal.ts", - "./modules": "./modules/internal.ts" + ".": "./src/index.ts", + "./internal": "./src/internal.ts" }, "dependencies": { - "@better-auth/prisma-adapter": "^1.5.5", - "@cvsa/db": "*", - "@huggingface/hub": "^2.11.0", - "@huggingface/transformers": "^4.0.0", - "@t3-oss/env-core": "^0.13.10", - "better-auth": "^1.5.5", + "@better-auth/prisma-adapter": "^1.5.6", + "@cvsa/db": "workspace:*", + "@cvsa/embedding": "workspace:*", + "@cvsa/env": "workspace:*", + "@cvsa/logger": "workspace:*", + "@cvsa/observability": "workspace:*", + "@elysiajs/eden": "^1.4.9", + "better-auth": "^1.5.6", "meilisearch": "^0.56.0", - "nanoid": "^5.1.6", - "onnxruntime-node": "^1.24.3", - "remeda": "^2.33.6", + "nanoid": "^5.1.7", + "remeda": "^2.33.7", "zod": "^4.3.6" } } diff --git a/packages/core/modules/auth/betterAuth.ts b/packages/core/src/auth/betterAuth.ts similarity index 93% rename from packages/core/modules/auth/betterAuth.ts rename to packages/core/src/auth/betterAuth.ts index 3b7fddb..18f303b 100644 --- a/packages/core/modules/auth/betterAuth.ts +++ b/packages/core/src/auth/betterAuth.ts @@ -1,7 +1,7 @@ import { APIError, betterAuth } from "better-auth"; import { prismaAdapter } from "@better-auth/prisma-adapter"; -import { prisma } from "@cvsa/core/common"; -import { getRandomId } from "@cvsa/core/common"; +import { prisma } from "@cvsa/db"; +import { getRandomId } from "@cvsa/core/internal"; import { username } from "better-auth/plugins"; import { bearer } from "better-auth/plugins"; diff --git a/packages/core/modules/auth/index.ts b/packages/core/src/auth/index.ts similarity index 100% rename from packages/core/modules/auth/index.ts rename to packages/core/src/auth/index.ts diff --git a/packages/core/modules/auth/user/dto.ts b/packages/core/src/auth/user/dto.ts similarity index 97% rename from packages/core/modules/auth/user/dto.ts rename to packages/core/src/auth/user/dto.ts index 553cc2d..e2ed638 100644 --- a/packages/core/modules/auth/user/dto.ts +++ b/packages/core/src/auth/user/dto.ts @@ -1,7 +1,7 @@ import { z } from "zod"; -import type { auth } from "@cvsa/core"; -import { getRandomId } from "@cvsa/core/common"; +import { getRandomId } from "@cvsa/core/internal"; import type { User } from "@cvsa/db"; +import type { auth } from "@cvsa/core/internal"; type BetterAuthUser = Exclude>, null>["user"]; diff --git a/packages/core/modules/auth/user/index.ts b/packages/core/src/auth/user/index.ts similarity index 100% rename from packages/core/modules/auth/user/index.ts rename to packages/core/src/auth/user/index.ts diff --git a/packages/core/modules/catalog/index.ts b/packages/core/src/catalog/index.ts similarity index 100% rename from packages/core/modules/catalog/index.ts rename to packages/core/src/catalog/index.ts diff --git a/packages/core/modules/catalog/song/container.ts b/packages/core/src/catalog/song/container.ts similarity index 55% rename from packages/core/modules/catalog/song/container.ts rename to packages/core/src/catalog/song/container.ts index cfaf523..834c269 100644 --- a/packages/core/modules/catalog/song/container.ts +++ b/packages/core/src/catalog/song/container.ts @@ -1,8 +1,10 @@ -import { prisma } from "@cvsa/core/common"; +import { prisma } from "@cvsa/db"; import { SongRepository } from "./repository"; import { SongService } from "./service"; -import { SongSearchService, searchManager, embeddingManager } from "@cvsa/core/modules"; - +import { SongSearchService, searchManager } from "../../search"; +import type { EmbeddingApp } from "@cvsa/embedding"; +import { treaty } from "@elysiajs/eden"; +export const embeddingManager = treaty("localhost:14900"); export const songRepository = new SongRepository(prisma); export const songSearchService = new SongSearchService( songRepository, diff --git a/packages/core/modules/catalog/song/dto.ts b/packages/core/src/catalog/song/dto.ts similarity index 93% rename from packages/core/modules/catalog/song/dto.ts rename to packages/core/src/catalog/song/dto.ts index 99aaae6..390d298 100644 --- a/packages/core/modules/catalog/song/dto.ts +++ b/packages/core/src/catalog/song/dto.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import { SongSchema, SingerSchema, ArtistSchema, ArtistRoleSchema, SongTypeSchema } from "@cvsa/db"; -import type { Serialized } from "@cvsa/core/common"; +import { + SongSchema, + SingerSchema, + ArtistSchema, + ArtistRoleSchema, + SongTypeSchema, + type Serialized, +} from "@cvsa/db"; export type SongId = number; diff --git a/packages/core/modules/catalog/song/index.ts b/packages/core/src/catalog/song/index.ts similarity index 100% rename from packages/core/modules/catalog/song/index.ts rename to packages/core/src/catalog/song/index.ts diff --git a/packages/core/modules/catalog/song/repository.interface.ts b/packages/core/src/catalog/song/repository.interface.ts similarity index 70% rename from packages/core/modules/catalog/song/repository.interface.ts rename to packages/core/src/catalog/song/repository.interface.ts index 22c9cf2..f19022c 100644 --- a/packages/core/modules/catalog/song/repository.interface.ts +++ b/packages/core/src/catalog/song/repository.interface.ts @@ -1,5 +1,5 @@ -import type { Song } from "@cvsa/db"; -import type { TxClient, IDetailsRepository, Serialized } from "@cvsa/core/common"; +import type { Song, TxClient, Serialized } from "@cvsa/db"; +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; import type { CreateSongRequestDto, SongId, @@ -7,7 +7,7 @@ import type { SongDetailsResponseDto, } from "./dto"; -export abstract class ISongRepository implements IDetailsRepository { +export abstract class ISongRepository implements IRepositoryWithGetDetails { abstract getById(id: SongId, tx?: TxClient): Promise | null>; abstract getDetailsById(id: SongId, tx?: TxClient): Promise; abstract create(input: CreateSongRequestDto, tx?: TxClient): Promise>; diff --git a/packages/core/modules/catalog/song/repository.ts b/packages/core/src/catalog/song/repository.ts similarity index 97% rename from packages/core/modules/catalog/song/repository.ts rename to packages/core/src/catalog/song/repository.ts index 84e7eb7..648ace8 100644 --- a/packages/core/modules/catalog/song/repository.ts +++ b/packages/core/src/catalog/song/repository.ts @@ -6,7 +6,7 @@ import type { SongDetailsResponseDto, } from "./dto"; import type { ISongRepository } from "./repository.interface"; -import { transformPrismaResult, type TxClient } from "@cvsa/core/common"; +import { transformPrismaResult, type TxClient } from "@cvsa/db"; export class SongRepository implements ISongRepository { constructor(private readonly prisma: PrismaClient) {} diff --git a/packages/core/src/catalog/song/service.ts b/packages/core/src/catalog/song/service.ts new file mode 100644 index 0000000..95aade6 --- /dev/null +++ b/packages/core/src/catalog/song/service.ts @@ -0,0 +1,73 @@ +import type { SongSearchService } from "@cvsa/core/internal"; +import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import type { Song, Serialized } from "@cvsa/db"; +import type { + SongDetailsResponseDto, + SongId, + CreateSongRequestDto, + UpdateSongRequestDto, +} from "./dto"; +import type { ISongRepository } from "./repository.interface"; +import { traceTask } from "@cvsa/observability"; + +export class SongService implements IServiceWithGetDetails { + constructor( + private readonly repository: ISongRepository, + private readonly search: SongSearchService + ) {} + + async getDetails(id: SongId) { + return traceTask("db findOne song", async () => { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + return result; + }); + } + + async create(input: CreateSongRequestDto): Promise> { + const result = await traceTask("db create song", async () => { + return await this.repository.create(input); + }); + try { + this.search.sync(result.id); + } catch (e) { + // TODO: replace log + console.error(e); + } + return result; + } + + async update(id: SongId, input: UpdateSongRequestDto): Promise> { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + const result = await traceTask("db update song", async () => { + return await this.repository.update(id, input); + }); + try { + this.search.sync(id); + } catch (e) { + // TODO: Should we mark this as dirty and sync it later? + console.error(e); + } + return result; + } + + async delete(id: SongId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + await traceTask("db delete song", async () => { + return await this.repository.softDelete(id); + }); + try { + this.search.sync(id); + } catch (e) { + console.error(e); + } + } +} diff --git a/packages/core/common/error/AppError.ts b/packages/core/src/error/AppError.ts similarity index 100% rename from packages/core/common/error/AppError.ts rename to packages/core/src/error/AppError.ts diff --git a/packages/core/common/error/index.ts b/packages/core/src/error/index.ts similarity index 51% rename from packages/core/common/error/index.ts rename to packages/core/src/error/index.ts index 1777091..e973307 100644 --- a/packages/core/common/error/index.ts +++ b/packages/core/src/error/index.ts @@ -1 +1,2 @@ export * from "./AppError"; +export * from "./schema"; diff --git a/packages/core/common/schema/error.ts b/packages/core/src/error/schema.ts similarity index 85% rename from packages/core/common/schema/error.ts rename to packages/core/src/error/schema.ts index cf9d3b7..939a7d7 100644 --- a/packages/core/common/schema/error.ts +++ b/packages/core/src/error/schema.ts @@ -18,6 +18,8 @@ const errorCodes = [ export const ErrorResponseSchema = z.object({ code: z.enum(errorCodes), message: z.string().optional(), + i18n: z.record(z.string(), z.string()).optional(), + traceId: z.string().optional(), }); export type ErrorResponseDto = z.infer; diff --git a/packages/core/modules/index.ts b/packages/core/src/index.ts similarity index 50% rename from packages/core/modules/index.ts rename to packages/core/src/index.ts index 74a7cd7..b9fbd95 100644 --- a/packages/core/modules/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,6 @@ export * from "./auth"; export * from "./catalog"; export * from "./search"; +export * from "./utils"; +export * from "./types"; +export * from "./error"; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts new file mode 100644 index 0000000..b22f0bd --- /dev/null +++ b/packages/core/src/internal.ts @@ -0,0 +1,6 @@ +export * from "./catalog"; +export * from "./search"; +export * from "./auth"; +export * from "./types"; +export * from "./utils"; +export * from "./error"; diff --git a/packages/core/modules/search/catalog/index.ts b/packages/core/src/search/catalog/index.ts similarity index 100% rename from packages/core/modules/search/catalog/index.ts rename to packages/core/src/search/catalog/index.ts diff --git a/packages/core/modules/search/catalog/song.ts b/packages/core/src/search/catalog/song.ts similarity index 82% rename from packages/core/modules/search/catalog/song.ts rename to packages/core/src/search/catalog/song.ts index 9f1059a..e78f7a3 100644 --- a/packages/core/modules/search/catalog/song.ts +++ b/packages/core/src/search/catalog/song.ts @@ -1,7 +1,8 @@ import type { SongType } from "@cvsa/db"; -import { ISearchService } from "../search.interface"; +import { ISearchService } from "../interface"; import { unique, keys } from "remeda"; -import type { SongDetailsResponseDto } from "@cvsa/core"; +import type { SongDetailsResponseDto } from "../../catalog"; +import { appLogger } from "@cvsa/logger"; export interface SongSearchIndex { id: number; @@ -28,10 +29,6 @@ export class SongSearchService extends ISearchService { song: SongDetailsResponseDto, language: string ): Promise { - if (!this.embeddingManager) { - // TODO: Use unified Error object - throw new Error("Embedding service not available"); - } const getDesc = () => { if (language === song.language) return song.description; return song.localizedDescriptions?.[language]; @@ -54,14 +51,16 @@ export class SongSearchService extends ISearchService { const getArtists = () => { return getLocalizedName("artists"); }; - const vectors = await this.embeddingManager.getEmbedding([ - `Name: ${song.name ?? ""} + const vectors = await this.embeddingManager.embeddings.post({ + texts: [ + `Name: ${song.name ?? ""} Lyrics: ${song.lyrics ?? ""} Description: ${getDesc() ?? ""} Singers: ${getSingers().join(", ")} Artists: ${getArtists().join(", ")} `, - ]); + ], + }); return { id: song.id, name: song.name ?? undefined, @@ -76,15 +75,19 @@ Artists: ${getArtists().join(", ")} .map((item) => item.engine ?? undefined) .filter(Boolean) as string[], publishedAt: song.publishedAt ? new Date(song.publishedAt).getTime() : undefined, - _vectors: { - "potion-multilingual-128M": vectors[0], - }, + // TODO: Error handling. + _vectors: vectors.data?.embeddings[0] + ? { + "potion-multilingual-128M": vectors.data?.embeddings[0], + } + : undefined, }; } public async sync(id: number) { if (!this.manager) { - throw new Error("Search service not available"); + appLogger.warn("Search service not available"); + return; } const song = await this.repository.getDetailsById(id); @@ -112,13 +115,16 @@ Artists: ${getArtists().join(", ")} } public async search(query: string, language: string = "zh") { - if (!this.manager || !this.embeddingManager) { + if (!this.manager) { throw new Error("Search or embedding service not available"); } const index = this.manager.getSearchIndex(`song_${language}`); + const embeddingResponse = await this.embeddingManager.embeddings.post({ + texts: [query], + }); return index.search(query, { - vector: (await this.embeddingManager.getEmbedding([query]))[0], + vector: embeddingResponse?.data?.embeddings[0], hybrid: { embedder: "potion-multilingual-128M", semanticRatio: 0.25, diff --git a/packages/core/modules/search/config.ts b/packages/core/src/search/config.ts similarity index 100% rename from packages/core/modules/search/config.ts rename to packages/core/src/search/config.ts diff --git a/packages/core/modules/search/container.ts b/packages/core/src/search/container.ts similarity index 100% rename from packages/core/modules/search/container.ts rename to packages/core/src/search/container.ts diff --git a/packages/core/modules/search/index.ts b/packages/core/src/search/index.ts similarity index 65% rename from packages/core/modules/search/index.ts rename to packages/core/src/search/index.ts index 5bb3214..dd28cf7 100644 --- a/packages/core/modules/search/index.ts +++ b/packages/core/src/search/index.ts @@ -1,2 +1,3 @@ export * from "./catalog"; export * from "./container"; +export * from "./interface"; diff --git a/packages/core/src/search/interface.ts b/packages/core/src/search/interface.ts new file mode 100644 index 0000000..55d441d --- /dev/null +++ b/packages/core/src/search/interface.ts @@ -0,0 +1,14 @@ +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; +import type { SearchManager } from "./manager"; +import type { SearchResponse } from "meilisearch"; +import type { EmbeddingAppApi } from "@cvsa/embedding"; + +export abstract class ISearchService { + constructor( + protected readonly repository: IRepositoryWithGetDetails, + protected readonly manager: SearchManager | undefined, + protected readonly embeddingManager: EmbeddingAppApi + ) {} + abstract sync(id: number): Promise; + abstract search(query: string, language: string): Promise; +} diff --git a/packages/core/modules/search/manager.ts b/packages/core/src/search/manager.ts similarity index 98% rename from packages/core/modules/search/manager.ts rename to packages/core/src/search/manager.ts index 0c483ed..eb8903e 100644 --- a/packages/core/modules/search/manager.ts +++ b/packages/core/src/search/manager.ts @@ -1,7 +1,7 @@ import { MeiliSearch, type Settings, type RecordAny } from "meilisearch"; -import { env } from "@cvsa/core"; +import { env } from "@cvsa/env"; import { INDEX_SETTINGS } from "./config"; -import { deepEqualUnordered } from "@cvsa/core/common"; +import { deepEqualUnordered } from "@cvsa/core/internal"; interface SearchManagerConstructor { client: MeiliSearch; diff --git a/packages/core/common/types/index.ts b/packages/core/src/types/index.ts similarity index 67% rename from packages/core/common/types/index.ts rename to packages/core/src/types/index.ts index a3910f4..3dfbbec 100644 --- a/packages/core/common/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,3 +1,2 @@ -export * from "./TxClient"; export * from "./repository"; export * from "./service"; diff --git a/packages/core/src/types/repository.ts b/packages/core/src/types/repository.ts new file mode 100644 index 0000000..ac40ce5 --- /dev/null +++ b/packages/core/src/types/repository.ts @@ -0,0 +1,5 @@ +import type { TxClient } from "@cvsa/db"; + +export interface IRepositoryWithGetDetails { + getDetailsById(id: number | string, tx?: TxClient): Promise; +} diff --git a/packages/core/common/types/service.ts b/packages/core/src/types/service.ts similarity index 55% rename from packages/core/common/types/service.ts rename to packages/core/src/types/service.ts index 772a6a5..9741901 100644 --- a/packages/core/common/types/service.ts +++ b/packages/core/src/types/service.ts @@ -1,3 +1,3 @@ -export interface IDetailsService { +export interface IServiceWithGetDetails { getDetails(id: number | string): Promise; } diff --git a/packages/core/common/utils/deepEqualUnordered.ts b/packages/core/src/utils/deepEqualUnordered.ts similarity index 100% rename from packages/core/common/utils/deepEqualUnordered.ts rename to packages/core/src/utils/deepEqualUnordered.ts diff --git a/packages/core/common/utils/index.ts b/packages/core/src/utils/index.ts similarity index 100% rename from packages/core/common/utils/index.ts rename to packages/core/src/utils/index.ts diff --git a/packages/core/common/utils/randomId.ts b/packages/core/src/utils/randomId.ts similarity index 100% rename from packages/core/common/utils/randomId.ts rename to packages/core/src/utils/randomId.ts diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index 1db6ca1..7656344 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; import { songRepository, type CreateSongRequestDto, type UpdateSongRequestDto } from "@cvsa/core"; -import { prisma } from "@cvsa/core/common"; +import { prisma } from "@cvsa/db"; const repository = songRepository; diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index 476f6f5..bb8f5ae 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -1,11 +1,7 @@ import { describe, expect, mock, test } from "bun:test"; -import { SongService, AppError } from "@cvsa/core"; -import type { - SongDetailsResponseDto, - ISongRepository, - SongSearchService, - Serialized, -} from "@cvsa/core"; +import { SongService, AppError } from "@cvsa/core/internal"; +import type { SongDetailsResponseDto, ISongRepository, SongSearchService } from "@cvsa/core"; +import type { Serialized } from "@cvsa/db"; import type { Song } from "@cvsa/db"; import { createMockRepository } from "../utils"; @@ -62,7 +58,7 @@ describe("SongService", () => { mockRepository.getDetailsById.mockResolvedValue(null); expect(songService.getDetails(999)).rejects.toThrow(AppError); - expect(songService.getDetails(999)).rejects.toThrow("Song not found"); + expect(songService.getDetails(999)).rejects.toThrow("error.song.notfound"); expect(songService.getDetails(999)).rejects.toMatchObject({ code: "NOT_FOUND", statusCode: 404, diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index ee22f73..9beb816 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@cvsa/typescript-config/base.json", - "include": ["modules/**/*.ts", "tests/**/*.ts", "common/**/*.ts", "index.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } diff --git a/packages/core/turbo.json b/packages/core/turbo.json index 543a70e..a576182 100644 --- a/packages/core/turbo.json +++ b/packages/core/turbo.json @@ -2,14 +2,13 @@ "extends": ["//"], "tasks": { "lint": { - "inputs": ["tests/**.ts", "common/**.ts", "modules/**.ts", "biome.json", ".biome*"] + "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] }, "test": { "dependsOn": ["typecheck"], "inputs": [ "tests/**.ts", - "common/**.ts", - "modules/**.ts", + "src/**.ts", "$TURBO_ROOT$/packages/db/**/*.ts", "$TURBO_ROOT$/packages/db/**/*.prisma" ] @@ -17,8 +16,7 @@ "typecheck": { "inputs": [ "tests/**.ts", - "common/**.ts", - "modules/**.ts", + "src/**.ts", "tsconfig.json", "$TURBO_ROOT$/packages/typescript-config/base.json" ] diff --git a/packages/db/index.ts b/packages/db/index.ts deleted file mode 100644 index f2fe784..0000000 --- a/packages/db/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./prisma/generated/client"; -export * from "./prisma/generated/models"; -export * from "./prisma/generated/enums"; -export * from "./prisma/zod"; -export * from "./prisma/types"; diff --git a/packages/db/package.json b/packages/db/package.json index 5f8bdea..b2f8020 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,7 +1,8 @@ { "name": "@cvsa/db", "type": "module", - "module": "index.ts", + "main": "src/index.ts", + "module": "src/index.ts", "scripts": { "gen": "bunx --bun prisma generate", "migrate": "bunx --bun prisma migrate dev --create-only", @@ -9,8 +10,7 @@ "push": "bunx --bun prisma db push", "db:deploy": "bunx --bun prisma migrate deploy", "test:setup": "NODE_ENV=test bunx --bun prisma db push --accept-data-loss", - "studio": "bunx --bun prisma studio", - "typecheck": "bunx --bun tsc --noEmit" + "studio": "bunx --bun prisma studio" }, "devDependencies": { "@cvsa/prisma-zod": "^3.6.0", @@ -19,6 +19,7 @@ "prisma-json-types-generator": "^4.1.1" }, "dependencies": { + "@cvsa/env": "workspace:*", "@prisma/adapter-pg": "^7.6.0", "@prisma/client": "^7.6.0", "@types/pg": "^8.16.0", diff --git a/packages/core/common/types/TxClient.ts b/packages/db/src/index.ts similarity index 53% rename from packages/core/common/types/TxClient.ts rename to packages/db/src/index.ts index 71a0974..de280a4 100644 --- a/packages/core/common/types/TxClient.ts +++ b/packages/db/src/index.ts @@ -1,6 +1,9 @@ -import type { PrismaClient } from "@cvsa/db"; +import type { PrismaClient } from "./types"; export type TxClient = Omit< PrismaClient, "$transaction" | "$connect" | "$disconnect" | "$on" | "$use" >; + +export * from "./prisma"; +export * from "./types"; diff --git a/packages/core/common/prisma.ts b/packages/db/src/prisma.ts similarity index 78% rename from packages/core/common/prisma.ts rename to packages/db/src/prisma.ts index 0b99d5e..d6022a7 100644 --- a/packages/core/common/prisma.ts +++ b/packages/db/src/prisma.ts @@ -1,6 +1,6 @@ -import { PrismaClient } from "@cvsa/db"; +import { PrismaClient } from "./types"; import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "./env"; +import { env } from "@cvsa/env"; const adapter = new PrismaPg({ connectionString: env.DATABASE_URL, @@ -47,4 +47,12 @@ export function transformPrismaResult(obj: T): Serialized { return obj as Serialized; } -export const prisma = new PrismaClient({ adapter }); +export const prisma = new PrismaClient({ + adapter, + log: [ + { level: "query", emit: "event" }, + { level: "info", emit: "stdout" }, + { level: "warn", emit: "stdout" }, + { level: "error", emit: "stdout" }, + ], +}); diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts new file mode 100644 index 0000000..a987e80 --- /dev/null +++ b/packages/db/src/types.ts @@ -0,0 +1,5 @@ +export * from "../prisma/generated/client"; +export * from "../prisma/generated/models"; +export * from "../prisma/generated/enums"; +export * from "../prisma/zod"; +export * from "../prisma/types"; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 6a9bec0..45730ba 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@cvsa/typescript-config/base.json", - "include": ["index.ts"] + "include": ["./src/*.ts"] } diff --git a/packages/embedding/package.json b/packages/embedding/package.json new file mode 100644 index 0000000..0e412c7 --- /dev/null +++ b/packages/embedding/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cvsa/embedding", + "type": "module", + "module": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "start": "bun run src/index.ts", + "dev": "bun run --watch src/index.ts", + "test": "bun test" + }, + "dependencies": { + "@cvsa/logger": "workspace:*", + "@elysiajs/eden": "^1.4.9", + "@huggingface/hub": "^2.11.0", + "@huggingface/transformers": "^4.0.1", + "elysia": "latest", + "onnxruntime-node": "^1.24.3", + "zod": "^4.3.6" + }, + "devDependencies": { + "bun-types": "latest", + "@cvsa/typescript-config": "workspace:*" + } +} diff --git a/packages/core/modules/ml/embedding.ts b/packages/embedding/src/embedding.ts similarity index 56% rename from packages/core/modules/ml/embedding.ts rename to packages/embedding/src/embedding.ts index 446dd89..0943f7f 100644 --- a/packages/core/modules/ml/embedding.ts +++ b/packages/embedding/src/embedding.ts @@ -1,30 +1,37 @@ import path from "node:path"; import { AutoTokenizer, type PreTrainedTokenizer } from "@huggingface/transformers"; -import { downloadFile } from "@huggingface/hub"; +import { fileDownloadInfo } from "@huggingface/hub"; import * as ort from "onnxruntime-node"; +import { appLogger } from "@cvsa/logger"; -const modelDir = path.join(import.meta.dir, "../../../../model/"); +const modelDir = path.join(import.meta.dir, "../../../model/"); // TODO: More formalized config -const tokenizerModel = "minishlab/potion-multilingual-128M"; -const modelPath = path.join(modelDir, "./potion-quant/model.onnx"); +const modelName = "alikia2x/potion-multilingual-128M-int8-strip"; +const modelPath = path.join(modelDir, "./potion-strip/model.onnx"); // TODO: Unit test for this export class EmbeddingManager { private tokenizer: PreTrainedTokenizer | null = null; private session: ort.InferenceSession | null = null; - public async init(): Promise { - await this.downloadModel(); - await this.initTokenizer(); - await this.initSession(); + public async init(): Promise { + try { + await this.downloadModel(); + await this.initTokenizer(); + await this.initSession(); + return true; + } catch (e) { + appLogger.error(Bun.inspect(e)); + return false; + } } private async initTokenizer(): Promise { if (this.tokenizer !== null) { return; } - this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerModel); + this.tokenizer = await AutoTokenizer.from_pretrained(modelName); } private async initSession(): Promise { @@ -36,7 +43,7 @@ export class EmbeddingManager { private async getTokenizer(): Promise { if (this.tokenizer === null) { - this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerModel); + this.tokenizer = await AutoTokenizer.from_pretrained(modelName); } return this.tokenizer; } @@ -62,23 +69,58 @@ export class EmbeddingManager { return result; } - // TODO: progress indicator private async downloadModel() { const modelFile = Bun.file(modelPath); if (await modelFile.exists()) { return; } - console.log("Downloading embedding model..."); - const blob = await downloadFile({ - repo: "alikia2x/potion-multilingual-128M-int8", + appLogger.info("Downloading embedding model..."); + + const info = await fileDownloadInfo({ + repo: modelName, path: "onnx/model.onnx", }); - if (!blob) { - throw new Error("Cannot download model file."); + + if (!info) { + appLogger.error("Cannot get download info for model file."); + return; } - const buffer = await blob.arrayBuffer(); - console.log("Embedding model downloaded."); - modelFile.write(buffer); + + const { url, size } = info; + let downloaded = 0; + + const response = await fetch(url); + if (!response.ok) { + appLogger.error(`Failed to download model: ${response.status} ${response.statusText}`); + return; + } + + if (!response.body) { + appLogger.error("Response body is null"); + return; + } + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + downloaded += value.byteLength; + const percent = size > 0 ? Math.round((downloaded / size) * 100) : 0; + appLogger.info(`Embedding model download: ${percent}% (${downloaded}/${size} bytes)`); + chunks.push(value); + } + + const buffer = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.byteLength; + } + + await Bun.write(modelFile, buffer); + appLogger.info("Embedding model downloaded."); } public async getEmbedding(texts: string[]): Promise { @@ -120,5 +162,6 @@ export class EmbeddingManager { } } -export const embeddingManager = new EmbeddingManager(); -await embeddingManager.init(); +const manager = new EmbeddingManager(); +const success = await manager.init(); +export const embeddingManager = success ? manager : undefined; diff --git a/packages/embedding/src/index.ts b/packages/embedding/src/index.ts new file mode 100644 index 0000000..b2df098 --- /dev/null +++ b/packages/embedding/src/index.ts @@ -0,0 +1,35 @@ +import { Elysia } from "elysia"; +import z from "zod"; +import { embeddingManager } from "./embedding"; +import type { treaty } from "@elysiajs/eden"; + +export const app = new Elysia() + .post( + "/embeddings", + async ({ body, status }) => { + const { texts } = body; + const embeddings = await embeddingManager?.getEmbedding(texts); + if (!embeddings) { + return status(500, { message: "cannot generate embedding" }); + } + return status(200, { + embeddings, + }); + }, + { + body: z.object({ + texts: z.string().array(), + }), + response: { + 200: z.object({ + embeddings: z.number().array().array(), + }), + 500: z.object({ + message: z.string(), + }), + }, + } + ) + .listen(14900); +export type EmbeddingApp = typeof app; +export type EmbeddingAppApi = ReturnType>; diff --git a/packages/embedding/tests/e2e.test.ts b/packages/embedding/tests/e2e.test.ts new file mode 100644 index 0000000..e1ae526 --- /dev/null +++ b/packages/embedding/tests/e2e.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "../src/"; + +const api = treaty(app); + +describe("Embedding E2E Tests - POST /embeddings", () => { + test("should generate embeddings", async () => { + const payload = { + texts: ["Hello"], + }; + + const { data, status } = await api.embeddings.post(payload); + + expect(status).toBe(200); + expect(data?.embeddings[0]).toHaveLength(256); + }); +}); diff --git a/packages/embedding/tsconfig.json b/packages/embedding/tsconfig.json new file mode 100644 index 0000000..8417383 --- /dev/null +++ b/packages/embedding/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cvsa/typescript-config/base.json", + "include": ["./src/*.ts", "./tests/*.ts"] +} diff --git a/packages/env/package.json b/packages/env/package.json new file mode 100644 index 0000000..6c9d59a --- /dev/null +++ b/packages/env/package.json @@ -0,0 +1,18 @@ +{ + "name": "@cvsa/env", + "type": "module", + "main": "src/index.ts", + "module": "src/index.ts", + "version": "0.1.0", + "scripts": { + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "latest", + "@cvsa/typescript-config": "workspace:*" + }, + "dependencies": { + "@t3-oss/env-core": "^0.13.11", + "zod": "^4.3.6" + } +} diff --git a/packages/core/common/env.ts b/packages/env/src/index.ts similarity index 59% rename from packages/core/common/env.ts rename to packages/env/src/index.ts index e53d903..26231f0 100644 --- a/packages/core/common/env.ts +++ b/packages/env/src/index.ts @@ -3,10 +3,15 @@ import { z } from "zod"; export const env = createEnv({ server: { - DATABASE_URL: z.url(), + DATABASE_URL: z.url().optional(), NODE_ENV: z.enum(["development", "test", "production"]).optional(), MEILI_MASTER_KEY: z.string().optional().default(""), MEILI_API_URL: z.url().optional().default("http://127.0.0.1:7700"), + + OTEL_SERVICE_NAME: z.string().optional().default("cvsa-backend"), + OTEL_SERVICE_VERSION: z.string().optional().default("0.0.0"), + + LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error"]).optional().default("info"), }, runtimeEnv: import.meta.env, diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json new file mode 100644 index 0000000..9ed01c6 --- /dev/null +++ b/packages/env/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cvsa/typescript-config/base.json", + "include": ["src/**/*.ts", "src/*.ts"] +} diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..4125126 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,29 @@ +{ + "name": "@cvsa/logger", + "type": "module", + "main": "src/index.ts", + "module": "src/index.ts", + "version": "0.1.0", + "scripts": { + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "latest", + "@cvsa/typescript-config": "workspace:*" + }, + "dependencies": { + "@cvsa/env": "workspace:*", + "@cvsa/observability": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-logs": "^0.214.0", + "@opentelemetry/sdk-node": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@t3-oss/env-core": "^0.13.11", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", + "zod": "^4.3.6" + } +} diff --git a/packages/logger/src/appLogger.ts b/packages/logger/src/appLogger.ts new file mode 100644 index 0000000..b10d1f7 --- /dev/null +++ b/packages/logger/src/appLogger.ts @@ -0,0 +1,140 @@ +import type { Logger as PinoLogger } from "pino"; +import { type Logger, SeverityNumber } from "@opentelemetry/api-logs"; +import { trace, SpanStatusCode, type Attributes } from "@opentelemetry/api"; +import { pinoLogger } from "./pinoSetup"; +import { otelLogger } from "@cvsa/observability"; + +type PinoLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; +type LogAttrs = Record | undefined; + +interface ApplicationLogger { + trace(msg: string, attrs?: LogAttrs): void; + debug(msg: string, attrs?: LogAttrs): void; + info(msg: string, attrs?: LogAttrs): void; + warn(msg: string, attrs?: LogAttrs): void; + error(msg: string, attrs?: LogAttrs): void; + fatal(msg: string, attrs?: LogAttrs): void; +} + +const PINO_TO_OTEL_LEVEL: Record = { + trace: SeverityNumber.TRACE, + debug: SeverityNumber.DEBUG, + info: SeverityNumber.INFO, + warn: SeverityNumber.WARN, + error: SeverityNumber.ERROR, + fatal: SeverityNumber.FATAL, +}; + +function getSpanContext(): { traceId: string | undefined; spanId: string | undefined } { + const activeSpan = trace.getActiveSpan(); + if (!activeSpan) { + return { traceId: undefined, spanId: undefined }; + } + const spanContext = activeSpan.spanContext(); + return { + traceId: spanContext.traceId || undefined, + spanId: spanContext.spanId || undefined, + }; +} + +function createLogMethod(level: PinoLevel) { + return function logMethod( + pinoLogger: PinoLogger, + otelLogger: Logger | undefined, + msg: string, + attrs?: LogAttrs + ): void { + const { traceId, spanId } = getSpanContext(); + const activeSpan = trace.getActiveSpan(); + + const spanContextAttrs = + traceId && spanId ? { trace_id: traceId, span_id: spanId } : undefined; + + const mergedAttrs = { + ...attrs, + ...spanContextAttrs, + }; + + const pinoMethod = pinoLogger[level].bind(pinoLogger) as ( + msg: string, + ...args: unknown[] + ) => void; + if (level === "error" || level === "fatal") { + const err = attrs && "error" in attrs ? (attrs.error as Error) : undefined; + if (err instanceof Error) { + pinoMethod(msg, { + ...mergedAttrs, + err, + }); + } else { + pinoMethod(msg, mergedAttrs); + } + } else { + pinoMethod(msg, mergedAttrs); + } + + if (otelLogger) { + const otelAttrs: Attributes = { + ...(attrs || {}), + ...(spanContextAttrs || {}), + }; + + if (level === "error" || level === "fatal") { + const err = attrs && "error" in attrs ? (attrs.error as Error) : undefined; + if (err) { + otelAttrs["error.object"] = String(err); + } + } + + otelLogger.emit({ + severityText: level, + severityNumber: PINO_TO_OTEL_LEVEL[level], + body: msg, + attributes: otelAttrs, + }); + } + + if (activeSpan) { + activeSpan.addEvent(msg, attrs as Attributes); + + if (level === "error" || level === "fatal") { + activeSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg }); + } + } + }; +} + +export function createApplicationLogger( + pinoLogger: PinoLogger, + otelLogger: Logger | undefined +): ApplicationLogger { + const traceMethod = createLogMethod("trace"); + const debugMethod = createLogMethod("debug"); + const infoMethod = createLogMethod("info"); + const warnMethod = createLogMethod("warn"); + const errorMethod = createLogMethod("error"); + const fatalMethod = createLogMethod("fatal"); + + return { + trace(msg: string, attrs?: LogAttrs): void { + traceMethod(pinoLogger, otelLogger, msg, attrs); + }, + debug(msg: string, attrs?: LogAttrs): void { + debugMethod(pinoLogger, otelLogger, msg, attrs); + }, + info(msg: string, attrs?: LogAttrs): void { + infoMethod(pinoLogger, otelLogger, msg, attrs); + }, + warn(msg: string, attrs?: LogAttrs): void { + warnMethod(pinoLogger, otelLogger, msg, attrs); + }, + error(msg: string, attrs?: LogAttrs): void { + errorMethod(pinoLogger, otelLogger, msg, attrs); + }, + fatal(msg: string, attrs?: LogAttrs): void { + fatalMethod(pinoLogger, otelLogger, msg, attrs); + }, + }; +} + +export const appLogger = createApplicationLogger(pinoLogger, otelLogger); diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000..cbb88d3 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,11 @@ +export { pinoLogger } from "./pinoSetup"; +export { appLogger } from "./appLogger"; + +export type { + Severity, + LogAttributes, + RequestLogAttrs, + ApplicationLogger, + RequestLogger, + OtelSdkInitResult, +} from "./types"; diff --git a/packages/logger/src/pinoSetup.ts b/packages/logger/src/pinoSetup.ts new file mode 100644 index 0000000..4d2aae0 --- /dev/null +++ b/packages/logger/src/pinoSetup.ts @@ -0,0 +1,49 @@ +import { env } from "@cvsa/env"; +import pino, { stdTimeFunctions } from "pino"; + +function createPinoLogger(env: { LOG_LEVEL: string; NODE_ENV: string | undefined }): pino.Logger { + const isProduction = env.NODE_ENV === "production"; + const isTest = env.NODE_ENV === "test"; + + const redact = ["authorization", "cookie", "password", "token"]; + + const timestamp = stdTimeFunctions.isoTime; + + const baseOptions = { + level: env.LOG_LEVEL || "info", + redact, + timestamp, + formatters: { + level: (label: string) => ({ level: label }), + }, + }; + + if (isProduction) { + return pino(baseOptions); + } + + if (isTest) { + return pino({ + ...baseOptions, + level: "warn", + transport: { + target: "pino-pretty", + options: { colorize: true }, + }, + }); + } + + return pino({ + ...baseOptions, + level: "debug", + transport: { + target: "pino-pretty", + options: { colorize: true }, + }, + }); +} + +export const pinoLogger = createPinoLogger({ + LOG_LEVEL: env.LOG_LEVEL, + NODE_ENV: env.NODE_ENV, +}); diff --git a/packages/logger/src/types.ts b/packages/logger/src/types.ts new file mode 100644 index 0000000..c7b61ce --- /dev/null +++ b/packages/logger/src/types.ts @@ -0,0 +1,52 @@ +export type Severity = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; + +export interface LogAttributes extends Record { + userId?: string; + requestId?: string; + traceId?: string; + spanId?: string; + [key: string]: unknown; +} + +export interface RequestLogAttrs { + method: string; + path: string; + status: number; + latency: string; + query?: Record; + userAgent?: string; + ip?: string; + requestBody?: unknown; +} + +export interface ApplicationLogger { + trace(msg: string, attrs?: LogAttributes): void; + debug(msg: string, attrs?: LogAttributes): void; + info(msg: string, attrs?: LogAttributes): void; + warn(msg: string, attrs?: LogAttributes): void; + error(msg: string, attrs?: LogAttributes): void; + fatal(msg: string, attrs?: LogAttributes): void; + child(initialAttrs: LogAttributes): ApplicationLogger; +} + +export interface RequestLogger { + log(request: RequestInfo, response: ResponseInfo, duration: number): void; +} + +export interface RequestInfo { + method: string; + path: string; + query: Record; + headers: Headers; + body: unknown; +} + +export interface ResponseInfo { + status: number; + body: unknown; +} + +export interface OtelSdkInitResult { + isAvailable: boolean; + reason?: string; +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..9ed01c6 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cvsa/typescript-config/base.json", + "include": ["src/**/*.ts", "src/*.ts"] +} diff --git a/packages/observability/package.json b/packages/observability/package.json new file mode 100644 index 0000000..5e48958 --- /dev/null +++ b/packages/observability/package.json @@ -0,0 +1,21 @@ +{ + "name": "@cvsa/observability", + "type": "module", + "main": "src/index.ts", + "module": "src/index.ts", + "version": "0.1.0", + "scripts": { + "typecheck": "bunx --bun tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "latest", + "@cvsa/typescript-config": "workspace:*" + }, + "dependencies": { + "@cvsa/env": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-node": "^0.214.0" + } +} diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts new file mode 100644 index 0000000..39aa871 --- /dev/null +++ b/packages/observability/src/index.ts @@ -0,0 +1,2 @@ +export * from "./init"; +export * from "./tracer"; diff --git a/packages/observability/src/init.ts b/packages/observability/src/init.ts new file mode 100644 index 0000000..ac462f5 --- /dev/null +++ b/packages/observability/src/init.ts @@ -0,0 +1,20 @@ +import { logs } from "@opentelemetry/api-logs"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { env } from "@cvsa/env"; + +export async function initOtelSdk() { + const resource = resourceFromAttributes({ + "service.name": env.OTEL_SERVICE_NAME, + "service.version": env.OTEL_SERVICE_VERSION, + "deployment.environment": env.NODE_ENV ?? "development", + }); + + const sdk = new NodeSDK({ + resource, + }); + + sdk.start(); +} + +export const otelLogger = logs.getLogger(env.OTEL_SERVICE_NAME || "cvsa"); diff --git a/packages/observability/src/tracer.ts b/packages/observability/src/tracer.ts new file mode 100644 index 0000000..d823970 --- /dev/null +++ b/packages/observability/src/tracer.ts @@ -0,0 +1,18 @@ +import { SpanStatusCode, trace } from "@opentelemetry/api"; +import pkg from "../package.json"; + +export const tracer = trace.getTracer(pkg.name, pkg.version); + +export async function traceTask(name: string, fn: () => T, startTime?: number): Promise { + return tracer.startActiveSpan(name, { startTime }, async (span) => { + try { + span.setStatus({ code: SpanStatusCode.OK }); + return await fn(); + } catch (e) { + span.setStatus({ code: SpanStatusCode.ERROR }); + throw e; + } finally { + span.end(); + } + }); +} diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json new file mode 100644 index 0000000..9ed01c6 --- /dev/null +++ b/packages/observability/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cvsa/typescript-config/base.json", + "include": ["src/**/*.ts", "src/*.ts"] +} diff --git a/turbo.json b/turbo.json index 922fba8..a18aab5 100644 --- a/turbo.json +++ b/turbo.json @@ -13,8 +13,17 @@ "cache": false, "persistent": true }, - "lint": {}, - "test": {}, + "lint": { + "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] + }, + "test": { + "inputs": [ + "src/**.ts", + "tests/**.ts", + "tsconfig.json", + "$TURBO_ROOT$/packages/typescript-config/base.json" + ] + }, "test:coverage": {}, "setup": { "cache": false, @@ -32,6 +41,13 @@ }, "@cvsa/db#gen": { "cache": false + }, + "typecheck": { + "inputs": [ + "src/**.ts", + "tsconfig.json", + "$TURBO_ROOT$/packages/typescript-config/base.json" + ] } } } From 1d5bef65b18ea83cb9c81153040e82adde3fed74 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sat, 11 Apr 2026 18:07:24 +0800 Subject: [PATCH 13/37] test: improve test coverage for search (#42) Added more tests for repositories, services, as well as the search module. --- .tokeignore | 1 - AGENTS.md | 7 +- apps/backend/bunfig.toml | 13 + apps/backend/src/errorHandler.ts | 17 +- apps/backend/tests/auth.test.ts | 17 +- apps/backend/tests/song.test.ts | 39 +- bun.lock | 11 +- locale/modules/backend/messages/en.json | 13 +- locale/modules/backend/messages/zh.json | 11 +- packages/core/bunfig.toml | 13 + packages/core/package.json | 6 +- packages/core/src/index.ts | 3 +- packages/core/src/internal.ts | 3 +- .../core/src/{ => modules}/auth/betterAuth.ts | 0 packages/core/src/{ => modules}/auth/index.ts | 0 .../core/src/{ => modules}/auth/user/dto.ts | 0 .../core/src/{ => modules}/auth/user/index.ts | 0 .../core/src/{ => modules}/catalog/index.ts | 0 .../{ => modules}/catalog/song/container.ts | 2 +- .../src/{ => modules}/catalog/song/dto.ts | 7 +- .../src/{ => modules}/catalog/song/index.ts | 0 .../catalog/song/repository.interface.ts | 0 .../{ => modules}/catalog/song/repository.ts | 3 + .../src/{ => modules}/catalog/song/service.ts | 34 +- packages/core/src/modules/index.ts | 2 + packages/core/src/search/catalog/song.ts | 29 +- packages/core/src/search/interface.ts | 2 +- packages/core/src/search/manager.ts | 98 +++-- .../tests/integration/SongRepository.test.ts | 86 +++- .../integration/SongSearchService.test.ts | 223 ++++++++++ .../core/tests/unit/SearchManager.test.ts | 385 ++++++++++++++++ .../core/tests/unit/SongSearchService.test.ts | 413 ++++++++++++++++++ packages/core/tests/unit/SongService.test.ts | 128 +++++- packages/core/tests/utils.ts | 2 + packages/db/package.json | 3 +- packages/db/prisma/models/core/artist.prisma | 2 +- .../db/prisma/models/core/artistRole.prisma | 4 +- packages/db/prisma/models/core/singer.prisma | 2 +- packages/logger/package.json | 9 +- packages/logger/src/pinoSetup.ts | 8 +- 40 files changed, 1459 insertions(+), 137 deletions(-) create mode 100644 apps/backend/bunfig.toml create mode 100644 packages/core/bunfig.toml rename packages/core/src/{ => modules}/auth/betterAuth.ts (100%) rename packages/core/src/{ => modules}/auth/index.ts (100%) rename packages/core/src/{ => modules}/auth/user/dto.ts (100%) rename packages/core/src/{ => modules}/auth/user/index.ts (100%) rename packages/core/src/{ => modules}/catalog/index.ts (100%) rename packages/core/src/{ => modules}/catalog/song/container.ts (88%) rename packages/core/src/{ => modules}/catalog/song/dto.ts (93%) rename packages/core/src/{ => modules}/catalog/song/index.ts (100%) rename packages/core/src/{ => modules}/catalog/song/repository.interface.ts (100%) rename packages/core/src/{ => modules}/catalog/song/repository.ts (94%) rename packages/core/src/{ => modules}/catalog/song/service.ts (76%) create mode 100644 packages/core/src/modules/index.ts create mode 100644 packages/core/tests/integration/SongSearchService.test.ts create mode 100644 packages/core/tests/unit/SearchManager.test.ts create mode 100644 packages/core/tests/unit/SongSearchService.test.ts diff --git a/.tokeignore b/.tokeignore index ac5f108..fbc5449 100644 --- a/.tokeignore +++ b/.tokeignore @@ -1,4 +1,3 @@ -prisma/ bruno/ *.json *.sql diff --git a/AGENTS.md b/AGENTS.md index 440f618..42dd3f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,15 +25,16 @@ When working on the backend, you MUST adhere to the Layered Architecture: `Handl ### File Organization Mapping If building a module named `[module]`, enforce this strict tree: ```text -apps/backend/src/modules/[module]/ +apps/backend/src/handlers/[module]/ ├── index.ts # Exports handlers -└── [handler].ts # Elysia routes +└── [method].ts # Elysia routes -packages/core/modules/[module]/ +packages/core/src/modules/[module]/ ├── index.ts # Public API exports ├── dto.ts # Zod schemas (Validation & Types) ├── service.ts # Business logic ├── repository.ts # Prisma DB operations +├── container.ts # DI container └── repository.interface.ts # DB contract ``` diff --git a/apps/backend/bunfig.toml b/apps/backend/bunfig.toml new file mode 100644 index 0000000..3fc94a4 --- /dev/null +++ b/apps/backend/bunfig.toml @@ -0,0 +1,13 @@ +[test] +coveragePathIgnorePatterns = [ + "**/db/prisma/**", + "**/db/src/**", + "**/env/src/**", + "**/logger/src/**", + "**/modules/auth/**", + "src/error/**", + "**/**.interface.ts", + "src/utils/randomId.ts", + "src/types/**", + "**/observability/src/**", +] \ No newline at end of file diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index f984aad..f1600ec 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -78,19 +78,24 @@ export const errorHandler = new Elysia() message: "error.not-found", }); } else if (code === "VALIDATION") { - const detail = error.detail(error.message); - const message = typeof detail === "string" ? detail : detail.summary; + // const detail = error.detail(error.message); + // const message = typeof detail === "string" ? detail : detail.summary; setErrorResponse(422, { code: "VALIDATION_ERROR", - message, + message: "error.validation", }); } else if (error instanceof BetterAuthAPIError) { const bodyCode = error.body?.code || ""; - if (AUTH_CONFLICT_CODES.includes(bodyCode as AuthConflictCode)) { + if (bodyCode === "USERNAME_IS_ALREADY_TAKEN") { setErrorResponse(409, { - code: (error.body?.code || "ENTITY_CONFLICT") as ErrorResponseDto["code"], - message: error.body?.message, + code: bodyCode, + message: "error.username-taken", + }); + } else if (bodyCode === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") { + setErrorResponse(409, { + code: bodyCode, + message: "error.email-taken", }); } else if (AUTH_INVALID_CODES.includes(bodyCode as AuthInvalidCode)) { setErrorResponse(401, { diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index 8fdfa40..20fab02 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,7 +6,8 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Registration E2E Tests - POST /v2/user", () => { - beforeEach(async () => { + beforeAll(async () => { + await prisma.$connect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); }); @@ -21,7 +22,7 @@ describe("Registration E2E Tests - POST /v2/user", () => { const payload = { username: "new_user", password: "password123", - email: "first@example.com", + email: `${Math.random()}@example.com`, }; const { data, status } = await api.v2.user.post(payload); @@ -112,7 +113,8 @@ describe("Registration E2E Tests - POST /v2/user", () => { }); describe("Profile E2E Tests - GET /v2/me", () => { - beforeEach(async () => { + beforeAll(async () => { + await prisma.$connect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); }); @@ -120,6 +122,7 @@ describe("Profile E2E Tests - GET /v2/me", () => { afterAll(async () => { await prisma.$disconnect(); }); + test("should return user profile with valid token", async () => { // 1. Register to get a token const signup = await api.v2.user.post({ @@ -184,7 +187,8 @@ describe("Profile E2E Tests - GET /v2/me", () => { }); describe("Login E2E Tests - POST /v2/session", () => { - beforeEach(async () => { + beforeAll(async () => { + await prisma.$connect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); }); @@ -273,7 +277,8 @@ describe("Login E2E Tests - POST /v2/session", () => { }); describe("Logout E2E Tests - DELETE /v2/session", () => { - beforeEach(async () => { + beforeAll(async () => { + await prisma.$connect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); }); diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts index f80c232..865f284 100644 --- a/apps/backend/tests/song.test.ts +++ b/apps/backend/tests/song.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,7 +6,8 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Song E2E Tests", () => { - beforeEach(async () => { + beforeAll(async () => { + await prisma.$connect(); await prisma.session.deleteMany(); await prisma.user.deleteMany(); await prisma.lyrics.deleteMany(); @@ -21,13 +22,43 @@ describe("Song E2E Tests", () => { async function getAuthToken() { const signup = await api.v2.user.post({ - username: "test_user", + username: `${Math.random()}`, password: "password123", - email: "test@example.com", }); return signup.data?.data.token; } + describe("GET /v2/song/:id/details - Get Song Details", async () => { + test("should retrieve a song", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }, + }); + + const { data, status } = await api.v2.song({ id: song.id }).details.get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + }); + + test("should return 404 for nonexistent song", async () => { + const { error, status } = await api.v2.song({ id: 1099 }).details.get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + describe("POST /v2/song - Create Song", () => { test("should create a song with authentication", async () => { const token = await getAuthToken(); diff --git a/bun.lock b/bun.lock index 9f1809c..ac29faa 100644 --- a/bun.lock +++ b/bun.lock @@ -120,15 +120,8 @@ "@cvsa/observability": "workspace:*", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/resources": "^2.6.1", - "@opentelemetry/sdk-logs": "^0.214.0", - "@opentelemetry/sdk-node": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@t3-oss/env-core": "^0.13.11", "pino": "^10.3.1", "pino-pretty": "^13.1.3", - "zod": "^4.3.6", }, "devDependencies": { "@cvsa/typescript-config": "workspace:*", @@ -499,7 +492,7 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -915,6 +908,8 @@ "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], diff --git a/locale/modules/backend/messages/en.json b/locale/modules/backend/messages/en.json index 31612ea..50e1538 100644 --- a/locale/modules/backend/messages/en.json +++ b/locale/modules/backend/messages/en.json @@ -1,9 +1,12 @@ { "error.unauthorized": "Unauthorized", - "error.song.notfound": "Song Not Found", - "error.signup.failed": "Failed to Sign Up", - "error.login.failed": "Failed to Log In", - "error.not-found": "Not Found", + "error.song.notfound": "This song does not exist.", + "error.signup.failed": "Unable to sign up. Please try again in a moment.", + "error.login.failed": "Unable to log in. Please try again in a moment.", + "error.not-found": "The requested resource was not found.", "error.invalid-cred": "Invalid Credentials", - "error.internal": "Internal Error" + "error.internal": "Internal Error", + "error.validation": "Invalid Input", + "error.username-taken": "This username is already taken.", + "error.email-taken": "This email is already registered." } diff --git a/locale/modules/backend/messages/zh.json b/locale/modules/backend/messages/zh.json index ef0ec63..d80d144 100644 --- a/locale/modules/backend/messages/zh.json +++ b/locale/modules/backend/messages/zh.json @@ -1,9 +1,12 @@ { "error.unauthorized": "未登录", - "error.song.notfound": "歌曲不存在", - "error.signup.failed": "无法注册", - "error.login.failed": "无法登录", + "error.song.notfound": "此歌曲不存在", + "error.signup.failed": "很抱歉,暂时无法注册", + "error.login.failed": "很抱歉,暂时无法登录", "error.not-found": "请求的路径不存在", "error.invalid-cred": "提交的凭据无效", - "error.internal": "服务器内部错误" + "error.internal": "服务器内部错误", + "error.validation": "输入信息有误", + "error.username-taken": "此用户名已被注册", + "error.email-taken": "此邮箱已被注册" } diff --git a/packages/core/bunfig.toml b/packages/core/bunfig.toml new file mode 100644 index 0000000..3fc94a4 --- /dev/null +++ b/packages/core/bunfig.toml @@ -0,0 +1,13 @@ +[test] +coveragePathIgnorePatterns = [ + "**/db/prisma/**", + "**/db/src/**", + "**/env/src/**", + "**/logger/src/**", + "**/modules/auth/**", + "src/error/**", + "**/**.interface.ts", + "src/utils/randomId.ts", + "src/types/**", + "**/observability/src/**", +] \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 6a8ce0c..1e3e5af 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,10 +6,10 @@ "version": "0.1.0", "scripts": { "lint": "bunx --bun biome lint", - "test": "bun test", + "test": "bun test --concurrent", "test:coverage": "bun test --coverage", - "test:unit": "bun test tests/unit", - "test:integration": "bun test tests/integration", + "test:unit": "bun test tests/unit --concurrent", + "test:integration": "bun test tests/integration --concurrent", "typecheck": "bunx --bun tsc --noEmit" }, "devDependencies": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b9fbd95..ae044e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,4 @@ -export * from "./auth"; -export * from "./catalog"; +export * from "./modules"; export * from "./search"; export * from "./utils"; export * from "./types"; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index b22f0bd..7001a41 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -1,6 +1,5 @@ -export * from "./catalog"; +export * from "./modules"; export * from "./search"; -export * from "./auth"; export * from "./types"; export * from "./utils"; export * from "./error"; diff --git a/packages/core/src/auth/betterAuth.ts b/packages/core/src/modules/auth/betterAuth.ts similarity index 100% rename from packages/core/src/auth/betterAuth.ts rename to packages/core/src/modules/auth/betterAuth.ts diff --git a/packages/core/src/auth/index.ts b/packages/core/src/modules/auth/index.ts similarity index 100% rename from packages/core/src/auth/index.ts rename to packages/core/src/modules/auth/index.ts diff --git a/packages/core/src/auth/user/dto.ts b/packages/core/src/modules/auth/user/dto.ts similarity index 100% rename from packages/core/src/auth/user/dto.ts rename to packages/core/src/modules/auth/user/dto.ts diff --git a/packages/core/src/auth/user/index.ts b/packages/core/src/modules/auth/user/index.ts similarity index 100% rename from packages/core/src/auth/user/index.ts rename to packages/core/src/modules/auth/user/index.ts diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/modules/catalog/index.ts similarity index 100% rename from packages/core/src/catalog/index.ts rename to packages/core/src/modules/catalog/index.ts diff --git a/packages/core/src/catalog/song/container.ts b/packages/core/src/modules/catalog/song/container.ts similarity index 88% rename from packages/core/src/catalog/song/container.ts rename to packages/core/src/modules/catalog/song/container.ts index 834c269..ee6bdf9 100644 --- a/packages/core/src/catalog/song/container.ts +++ b/packages/core/src/modules/catalog/song/container.ts @@ -1,7 +1,7 @@ import { prisma } from "@cvsa/db"; import { SongRepository } from "./repository"; import { SongService } from "./service"; -import { SongSearchService, searchManager } from "../../search"; +import { SongSearchService, searchManager } from "../../../search"; import type { EmbeddingApp } from "@cvsa/embedding"; import { treaty } from "@elysiajs/eden"; export const embeddingManager = treaty("localhost:14900"); diff --git a/packages/core/src/catalog/song/dto.ts b/packages/core/src/modules/catalog/song/dto.ts similarity index 93% rename from packages/core/src/catalog/song/dto.ts rename to packages/core/src/modules/catalog/song/dto.ts index 390d298..09fd5d4 100644 --- a/packages/core/src/catalog/song/dto.ts +++ b/packages/core/src/modules/catalog/song/dto.ts @@ -31,10 +31,13 @@ const CreateLyricsSchema = z.object({ }); export const CreateSongRequestSchema = z.object({ - type: SongTypeSchema.nullish(), name: z.string().nullish(), - duration: z.int().nullish(), + type: SongTypeSchema.nullish(), + language: z.string().nullish(), + localizedNames: z.record(z.string(), z.string()).nullish(), description: z.string().nullish(), + localizedDescriptions: z.record(z.string(), z.string()).nullish(), + duration: z.int().nullish(), coverUrl: z.url().nullish(), publishedAt: z.iso.datetime().nullish(), performances: z.array(CreatePerformanceSchema).optional(), diff --git a/packages/core/src/catalog/song/index.ts b/packages/core/src/modules/catalog/song/index.ts similarity index 100% rename from packages/core/src/catalog/song/index.ts rename to packages/core/src/modules/catalog/song/index.ts diff --git a/packages/core/src/catalog/song/repository.interface.ts b/packages/core/src/modules/catalog/song/repository.interface.ts similarity index 100% rename from packages/core/src/catalog/song/repository.interface.ts rename to packages/core/src/modules/catalog/song/repository.interface.ts diff --git a/packages/core/src/catalog/song/repository.ts b/packages/core/src/modules/catalog/song/repository.ts similarity index 94% rename from packages/core/src/catalog/song/repository.ts rename to packages/core/src/modules/catalog/song/repository.ts index 648ace8..0444a94 100644 --- a/packages/core/src/catalog/song/repository.ts +++ b/packages/core/src/modules/catalog/song/repository.ts @@ -81,8 +81,11 @@ export class SongRepository implements ISongRepository { return transformPrismaResult( await client.song.create({ data: { + language: songData.language ?? undefined, type: songData.type ?? null, name: songData.name ?? null, + localizedNames: songData.localizedNames ?? undefined, + localizedDescriptions: songData.localizedDescriptions ?? undefined, duration: songData.duration ?? null, description: songData.description ?? null, coverUrl: songData.coverUrl ?? null, diff --git a/packages/core/src/catalog/song/service.ts b/packages/core/src/modules/catalog/song/service.ts similarity index 76% rename from packages/core/src/catalog/song/service.ts rename to packages/core/src/modules/catalog/song/service.ts index 95aade6..7a62940 100644 --- a/packages/core/src/catalog/song/service.ts +++ b/packages/core/src/modules/catalog/song/service.ts @@ -9,6 +9,7 @@ import type { } from "./dto"; import type { ISongRepository } from "./repository.interface"; import { traceTask } from "@cvsa/observability"; +import { appLogger } from "@cvsa/logger"; export class SongService implements IServiceWithGetDetails { constructor( @@ -30,12 +31,11 @@ export class SongService implements IServiceWithGetDetails { return await this.repository.create(input); }); - try { - this.search.sync(result.id); - } catch (e) { - // TODO: replace log - console.error(e); - } + await traceTask("sync search index", async () => { + this.search.sync(result.id).catch((e) => { + appLogger.warn(Bun.inspect(e)); + }); + }); return result; } @@ -47,12 +47,12 @@ export class SongService implements IServiceWithGetDetails { return await this.repository.update(id, input); }); - try { - this.search.sync(id); - } catch (e) { - // TODO: Should we mark this as dirty and sync it later? - console.error(e); - } + await traceTask("sync search index", async () => { + this.search.sync(id).catch((e) => { + // TODO: Should we mark this as dirty and sync it later? + appLogger.warn(Bun.inspect(e)); + }); + }); return result; } @@ -64,10 +64,10 @@ export class SongService implements IServiceWithGetDetails { return await this.repository.softDelete(id); }); - try { - this.search.sync(id); - } catch (e) { - console.error(e); - } + await traceTask("sync search index", async () => { + this.search.sync(id).catch((e) => { + appLogger.warn(Bun.inspect(e)); + }); + }); } } diff --git a/packages/core/src/modules/index.ts b/packages/core/src/modules/index.ts new file mode 100644 index 0000000..7ac6bfd --- /dev/null +++ b/packages/core/src/modules/index.ts @@ -0,0 +1,2 @@ +export * from "./catalog"; +export * from "./auth"; diff --git a/packages/core/src/search/catalog/song.ts b/packages/core/src/search/catalog/song.ts index e78f7a3..d67ed7c 100644 --- a/packages/core/src/search/catalog/song.ts +++ b/packages/core/src/search/catalog/song.ts @@ -1,7 +1,7 @@ import type { SongType } from "@cvsa/db"; import { ISearchService } from "../interface"; import { unique, keys } from "remeda"; -import type { SongDetailsResponseDto } from "../../catalog"; +import type { SongDetailsResponseDto } from "../../modules"; import { appLogger } from "@cvsa/logger"; export interface SongSearchIndex { @@ -51,9 +51,13 @@ export class SongSearchService extends ISearchService { const getArtists = () => { return getLocalizedName("artists"); }; + const getName = () => { + if (language === song.language) return song.name; + return song.localizedNames?.[language]; + }; const vectors = await this.embeddingManager.embeddings.post({ texts: [ - `Name: ${song.name ?? ""} + `Name: ${getName() ?? ""} Lyrics: ${song.lyrics ?? ""} Description: ${getDesc() ?? ""} Singers: ${getSingers().join(", ")} @@ -63,12 +67,12 @@ Artists: ${getArtists().join(", ")} }); return { id: song.id, - name: song.name ?? undefined, + name: getName() ?? undefined, lyrics: getLyrics() ?? undefined, description: getDesc() ?? undefined, singers: getSingers(), artists: getArtists(), - bilibiliAid: Number(song.bilibiliAid) ?? undefined, + bilibiliAid: song.bilibiliAid ?? undefined, bilibiliBvid: song.bilibiliBvid ?? undefined, type: song.type ?? undefined, engine: song.singers @@ -85,16 +89,18 @@ Artists: ${getArtists().join(", ")} } public async sync(id: number) { - if (!this.manager) { + if (!this.searchManager) { appLogger.warn("Search service not available"); return; } const song = await this.repository.getDetailsById(id); if (!song) { - const indexesToBeDeleted = await this.manager.getLocalizedIndexesOfEntity("song"); + const indexesToBeDeleted = await this.searchManager.getLocalizedIndexesOfEntity("song"); for (const index of indexesToBeDeleted) { - await this.manager.getAdminIndex(index).deleteDocument(id); + const adminIndex = await this.searchManager.getAdminIndex(index); + const task = await adminIndex.deleteDocument(id); + await this.searchManager.waitForTask(task.taskUid); } return; } @@ -106,20 +112,21 @@ Artists: ${getArtists().join(", ")} ]); for (const language of languages) { const indexUid = `song_${language}`; - const index = this.manager.getAdminIndex(indexUid); + const index = await this.searchManager.getAdminIndex(indexUid); const document = await this.getDocument(song, language); - index.addDocuments([document], { + const task = await index.addDocuments([document], { primaryKey: "id", }); + await this.searchManager.waitForTask(task.taskUid); } } public async search(query: string, language: string = "zh") { - if (!this.manager) { + if (!this.searchManager) { throw new Error("Search or embedding service not available"); } - const index = this.manager.getSearchIndex(`song_${language}`); + const index = await this.searchManager.getSearchIndex(`song_${language}`); const embeddingResponse = await this.embeddingManager.embeddings.post({ texts: [query], }); diff --git a/packages/core/src/search/interface.ts b/packages/core/src/search/interface.ts index 55d441d..2b80ae5 100644 --- a/packages/core/src/search/interface.ts +++ b/packages/core/src/search/interface.ts @@ -6,7 +6,7 @@ import type { EmbeddingAppApi } from "@cvsa/embedding"; export abstract class ISearchService { constructor( protected readonly repository: IRepositoryWithGetDetails, - protected readonly manager: SearchManager | undefined, + protected readonly searchManager: SearchManager | undefined, protected readonly embeddingManager: EmbeddingAppApi ) {} abstract sync(id: number): Promise; diff --git a/packages/core/src/search/manager.ts b/packages/core/src/search/manager.ts index eb8903e..d9ab328 100644 --- a/packages/core/src/search/manager.ts +++ b/packages/core/src/search/manager.ts @@ -1,31 +1,41 @@ import { MeiliSearch, type Settings, type RecordAny } from "meilisearch"; import { env } from "@cvsa/env"; import { INDEX_SETTINGS } from "./config"; -import { deepEqualUnordered } from "@cvsa/core/internal"; +import { deepEqualUnordered } from "../utils"; +import { appLogger } from "@cvsa/logger"; -interface SearchManagerConstructor { - client: MeiliSearch; - adminClient: MeiliSearch; -} - -// TODO: Error handling & integration test export class SearchManager { - private readonly client: MeiliSearch; - private readonly adminClient: MeiliSearch; - - private constructor({ client, adminClient }: SearchManagerConstructor) { - this.client = client; - this.adminClient = adminClient; - this.syncAllSettings(); - } + private constructor( + private client?: MeiliSearch, + private adminClient?: MeiliSearch + ) {} /** * Creates a new SearchManager instance with separate search and admin clients. * Initializes MeiliSearch clients using API keys retrieved from the master client. * The search client has only search permissions, while the admin client has full permissions. + * Returns a SearchManager instance even if initialization fails - methods will retry initialization on each call. * @returns A promise that resolves to a new SearchManager instance */ - public static async create() { + public static async create(customClient?: MeiliSearch, customAdminClient?: MeiliSearch) { + if (customClient && customAdminClient) { + return new SearchManager(customClient, customAdminClient); + } + + const manager = new SearchManager(undefined, undefined); + await manager.initializeClients(); + return manager; + } + + /** + * Attempts to initialize the MeiliSearch clients. + * Safe to call multiple times - will retry if previous initialization failed. + */ + private async initializeClients(): Promise { + if (this.client !== undefined && this.adminClient !== undefined) { + return; + } + try { const masterClient = new MeiliSearch({ host: env.MEILI_API_URL, @@ -33,17 +43,25 @@ export class SearchManager { }); const adminKey = await SearchManager.getAdminKey(masterClient); const searchKey = await SearchManager.getSearchKey(masterClient); - const client = new MeiliSearch({ + (this as unknown as { client: MeiliSearch }).client = new MeiliSearch({ host: env.MEILI_API_URL, apiKey: searchKey, }); - const adminClient = new MeiliSearch({ + (this as unknown as { adminClient: MeiliSearch }).adminClient = new MeiliSearch({ host: env.MEILI_API_URL, apiKey: adminKey, }); - return new SearchManager({ client, adminClient }); - } catch { - return; + this.syncAllSettings(); + } catch (e) { + appLogger.warn("Cannot initialize SearchManager clients."); + appLogger.error(Bun.inspect(e)); + throw e; + } + } + + private ensureInitialized(): void { + if (this.client === undefined || this.adminClient === undefined) { + throw new Error("SearchManager not initialized"); } } @@ -93,7 +111,10 @@ export class SearchManager { * @returns A promise that resolves to the completed task */ public async waitForTask(taskUid: number) { - return await this.adminClient.tasks.waitForTask(taskUid, { + await this.initializeClients(); + this.ensureInitialized(); + const adminClient = this.adminClient as MeiliSearch; + return await adminClient.tasks.waitForTask(taskUid, { timeout: 10000, interval: 100, }); @@ -105,7 +126,10 @@ export class SearchManager { * @returns A promise that resolves to the task details */ public async getTask(taskUid: number) { - return await this.adminClient.tasks.getTask(taskUid); + await this.initializeClients(); + this.ensureInitialized(); + const adminClient = this.adminClient as MeiliSearch; + return await adminClient.tasks.getTask(taskUid); } /** @@ -114,8 +138,11 @@ export class SearchManager { * @param indexName - The name of the index * @returns The admin MeiliSearch index instance */ - public getAdminIndex(indexName: string) { - return this.adminClient.index(indexName); + public async getAdminIndex(indexName: string) { + await this.initializeClients(); + this.ensureInitialized(); + const adminClient = this.adminClient as MeiliSearch; + return adminClient.index(indexName); } /** @@ -124,8 +151,11 @@ export class SearchManager { * @param indexName - The name of the index * @returns The search-only MeiliSearch index instance */ - public getSearchIndex(indexName: string) { - return this.client.index(indexName); + public async getSearchIndex(indexName: string) { + await this.initializeClients(); + this.ensureInitialized(); + const client = this.client as MeiliSearch; + return client.index(indexName); } /** @@ -133,7 +163,10 @@ export class SearchManager { * @returns A promise that resolves to the list of all indexes */ public async listIndexes() { - return this.adminClient.getIndexes(); + await this.initializeClients(); + this.ensureInitialized(); + const adminClient = this.adminClient as MeiliSearch; + return adminClient.getIndexes(); } /** @@ -144,7 +177,10 @@ export class SearchManager { * @returns A promise that resolves to an array of matching index UIDs */ public async getLocalizedIndexesOfEntity(name: string) { - const { results: indexes } = await this.adminClient.getIndexes({ + await this.initializeClients(); + this.ensureInitialized(); + const adminClient = this.adminClient as MeiliSearch; + const { results: indexes } = await adminClient.getIndexes({ limit: 1000, }); const results: string[] = []; @@ -162,6 +198,10 @@ export class SearchManager { * Skips indexes without a corresponding entry in INDEX_SETTINGS. */ public async syncAllSettings() { + if (this.adminClient === undefined) { + return; + } + const { results: indexes } = await this.listIndexes(); for (const index of indexes) { const uid = index.uid; diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index 7656344..ee146c3 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -1,21 +1,30 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { songRepository, type CreateSongRequestDto, type UpdateSongRequestDto } from "@cvsa/core"; import { prisma } from "@cvsa/db"; const repository = songRepository; describe("SongRepository Integration Tests", () => { - beforeAll(async () => { - await prisma.$connect(); - }); - afterAll(async () => { - await prisma.song.deleteMany({ where: { deletedAt: null } }); + await prisma.creation.deleteMany(); + await prisma.artistRole.deleteMany(); + await prisma.artist.deleteMany(); + await prisma.performance.deleteMany(); + await prisma.lyrics.deleteMany(); + await prisma.singer.deleteMany(); + await prisma.song.deleteMany(); await prisma.$disconnect(); }); - beforeEach(async () => { - await prisma.song.deleteMany({ where: { deletedAt: null } }); + beforeAll(async () => { + await prisma.$connect(); + await prisma.creation.deleteMany(); + await prisma.artistRole.deleteMany(); + await prisma.artist.deleteMany(); + await prisma.performance.deleteMany(); + await prisma.lyrics.deleteMany(); + await prisma.singer.deleteMany(); + await prisma.song.deleteMany(); }); describe("create", () => { @@ -72,6 +81,67 @@ describe("SongRepository Integration Tests", () => { }); }); + describe("getDetailsById", () => { + test("should return song when exists", async () => { + const singer = await prisma.singer.create({ + data: { + name: "赤羽", + }, + }); + const artist = await prisma.artist.create({ + data: { + name: "月华P", + }, + }); + const artistRole = await prisma.artistRole.create({ + data: { + name: "作曲", + }, + }); + + const created = await repository.create({ + name: "尘海绘仙缘", + lyrics: [ + { + plainText: "捻笔指掌中 遥看日月变", + }, + ], + performances: [ + { + singerId: singer.id, + }, + ], + creations: [ + { + artistId: artist.id, + roleId: artistRole.id, + }, + ], + }); + const result = await repository.getDetailsById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("尘海绘仙缘"); + expect(result?.lyrics).toHaveLength(1); + expect(result?.lyrics[0]).toMatchObject({ + plainText: "捻笔指掌中 遥看日月变", + }); + expect(result?.artists).toHaveLength(1); + expect(result?.artists[0]).toMatchObject({ + name: "月华P", + role: { + name: "作曲", + }, + }); + }); + + test("should return null when song does not exist", async () => { + const result = await repository.getDetailsById(999999); + expect(result).toBeNull(); + }); + }); + describe("update", () => { test("should update all fields", async () => { const created = await repository.create({ name: "Old Name", type: "ORIGINAL" }); diff --git a/packages/core/tests/integration/SongSearchService.test.ts b/packages/core/tests/integration/SongSearchService.test.ts new file mode 100644 index 0000000..3bfbd96 --- /dev/null +++ b/packages/core/tests/integration/SongSearchService.test.ts @@ -0,0 +1,223 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { songRepository } from "@cvsa/core"; +import { prisma } from "@cvsa/db"; +import { SongSearchService } from "../../src/search/catalog/song"; +import type { EmbeddingAppApi } from "@cvsa/embedding"; +import { SearchManager } from "../../src/search/manager"; +import { env } from "@cvsa/env"; +import { MeiliSearch } from "meilisearch"; +// import { OriginalMeiliSearch } from "../utils"; +// import type { MeiliSearch as MeiliType } from "meilisearch"; + +// const MeiliSearch = OriginalMeiliSearch.MeiliSearch; +// console.debug("CCCCCCC", MeiliSearch); +// type MeiliSearch = MeiliType; + +const MEILI_HOST = env.MEILI_API_URL ?? "http://127.0.0.1:7700"; +const MEILI_MASTER_KEY = env.MEILI_MASTER_KEY ?? ""; + +const waitForTask = async (client: MeiliSearch, taskUid: number) => { + return client.tasks.waitForTask(taskUid, { timeout: 10000, interval: 100 }); +}; + +const cleanupIndex = async (client: MeiliSearch, indexName: string) => { + try { + const task = await client.deleteIndex(indexName); + await waitForTask(client, task.taskUid); + } catch { + // Index may not exist + } +}; + +const createIndexIfNotExists = async (client: MeiliSearch, indexName: string) => { + try { + await client.getIndex(indexName); + } catch { + const task = await client.createIndex(indexName, { primaryKey: "id" }); + await waitForTask(client, task.taskUid); + } +}; + +describe("SongSearchService Integration Tests", () => { + const client = new MeiliSearch({ host: MEILI_HOST, apiKey: MEILI_MASTER_KEY }); + let searchManager: SearchManager; + let mockEmbeddingManager: EmbeddingAppApi; + + beforeAll(async () => { + await prisma.$connect(); + await cleanupIndex(client, "song_zh"); + await cleanupIndex(client, "song_en"); + await createIndexIfNotExists(client, "song_zh"); + await createIndexIfNotExists(client, "song_en"); + + searchManager = await SearchManager.create(); + + mockEmbeddingManager = { + embeddings: { + post: async ({ texts }: { texts: string[] }) => { + const dimensions = 256; + const embeddings = texts.map(() => + Array.from({ length: dimensions }, () => Math.random()) + ); + return { data: { embeddings } } as never; + }, + }, + } as never; + }); + + beforeEach(async () => { + const indexZh = client.index("song_zh"); + const indexEn = client.index("song_en"); + try { + await indexZh.deleteAllDocuments(); + await indexEn.deleteAllDocuments(); + } catch { + // Ignore + } + }); + + afterAll(async () => { + await prisma.creation.deleteMany(); + await prisma.artistRole.deleteMany(); + await prisma.artist.deleteMany(); + await prisma.performance.deleteMany(); + await prisma.lyrics.deleteMany(); + await prisma.singer.deleteMany(); + await prisma.song.deleteMany(); + await prisma.$disconnect(); + }); + + describe("sync", () => { + test("syncs song to search index", async () => { + const singer = await prisma.singer.create({ + data: { name: "测试歌手", language: "zh" }, + }); + const artistRole = await prisma.artistRole.create({ + data: { name: "作曲" }, + }); + const artist = await prisma.artist.create({ + data: { name: "测试艺术家", language: "zh" }, + }); + + const song = await songRepository.create({ + name: "测试歌曲", + type: "ORIGINAL", + language: "zh", + description: "这是一首测试歌曲", + performances: [{ singerId: singer.id }], + creations: [{ artistId: artist.id, roleId: artistRole.id }], + }); + + const service = new SongSearchService( + songRepository, + searchManager, + mockEmbeddingManager + ); + await service.sync(song.id); + + const index = client.index("song_zh"); + const documents = await index.getDocuments(); + expect(documents.results.length).toBeGreaterThan(0); + + const doc = documents.results.find((d) => d.id === song.id); + expect(doc).toBeDefined(); + expect(doc?.name).toBe("测试歌曲"); + }); + + test("deletes song from search index when song is removed", async () => { + const song = await songRepository.create({ name: "待删除歌曲" }); + + const service = new SongSearchService( + songRepository, + searchManager, + mockEmbeddingManager + ); + await service.sync(song.id); + + const index = client.index("song_zh"); + let documents = await index.getDocuments(); + let doc = documents.results.find((d) => d.id === song.id); + expect(doc).toBeDefined(); + + await songRepository.softDelete(song.id); + + await service.sync(song.id); + + documents = await index.getDocuments(); + doc = documents.results.find((d) => d.id === song.id); + expect(doc).toBeUndefined(); + }); + + test("handles song with localized content", async () => { + const song = await songRepository.create({ + name: "多语言歌曲", + language: "zh", + localizedNames: { en: "Multi-language Song", ja: "多言語曲" }, + localizedDescriptions: { en: "English description" }, + }); + + const service = new SongSearchService( + songRepository, + searchManager, + mockEmbeddingManager + ); + await service.sync(song.id); + + const zhIndex = client.index("song_zh"); + const enIndex = client.index("song_en"); + + const zhDocs = await zhIndex.getDocuments(); + const enDocs = await enIndex.getDocuments(); + + const zhDoc = zhDocs.results.find((d) => d.id === song.id); + const enDoc = enDocs.results.find((d) => d.id === song.id); + + expect(zhDoc).toBeDefined(); + expect(enDoc).toBeDefined(); + expect(enDoc?.name).toBe("Multi-language Song"); + expect(enDoc?.description).toBe("English description"); + }); + }); + + describe("search", () => { + test("performs hybrid search", async () => { + const song = await songRepository.create({ + name: "混合搜索测试", + language: "zh", + description: "用于测试混合搜索功能", + }); + + const service = new SongSearchService( + songRepository, + searchManager, + mockEmbeddingManager + ); + await service.sync(song.id); + + const result = await service.search("混合搜索", "zh"); + + expect(result).toBeDefined(); + expect(result.hits).toBeDefined(); + }); + + test("throws error when search manager is not available", async () => { + const serviceWithoutManager = new SongSearchService( + songRepository, + undefined as never, + mockEmbeddingManager + ); + + expect(serviceWithoutManager.search("test")).rejects.toThrow( + "Search or embedding service not available" + ); + }); + }); +}); + +type SongSearchDocument = { + id: number; + name?: string; + description?: string; + singers?: string[]; + artists?: string[]; +}; diff --git a/packages/core/tests/unit/SearchManager.test.ts b/packages/core/tests/unit/SearchManager.test.ts new file mode 100644 index 0000000..c502d47 --- /dev/null +++ b/packages/core/tests/unit/SearchManager.test.ts @@ -0,0 +1,385 @@ +import { describe, expect, mock, test, beforeEach } from "bun:test"; +import type { Index, RecordAny, Task, MeiliSearch as MeiliSearchType } from "meilisearch"; +import { SearchManager } from "../../src/search/manager"; + +interface MockClient { + index: ReturnType; + getIndexes: ReturnType; + getKeys?: ReturnType; + createKey?: ReturnType; + tasks?: { + waitForTask: ReturnType; + getTask: ReturnType; + }; +} + +const mockGetKeys = mock(); +const mockCreateKey = mock(); +const mockGetIndexes = mock(); +const mockWaitForTask = mock(); +const mockGetTask = mock(); +const mockIndexGetSettings = mock(); +const mockIndexResetSettings = mock(); +const mockIndexUpdateSettings = mock(); + +const mockIndexInstance = { + getSettings: mockIndexGetSettings, + resetSettings: mockIndexResetSettings, + updateSettings: mockIndexUpdateSettings, + deleteDocument: mock(), + addDocuments: mock(), + search: mock(), +}; + +const createMockIndex = (uid: string, getSettingsReturn?: Record) => { + const settingsMock = mock(); + settingsMock.mockResolvedValue( + getSettingsReturn ?? { + searchableAttributes: [], + filterableAttributes: [], + sortableAttributes: [], + rankingRules: [], + } + ); + return { + uid, + getSettings: settingsMock, + resetSettings: mockIndexResetSettings, + updateSettings: mockIndexUpdateSettings, + deleteDocument: mock(), + addDocuments: mock(), + search: mock(), + }; +}; + +const createMockClient = (): { client: MockClient; adminClient: MockClient } => { + const client = { + index: mock(() => mockIndexInstance), + getIndexes: mockGetIndexes, + }; + const adminClient = { + index: mock(() => mockIndexInstance), + getIndexes: mockGetIndexes, + getKeys: mockGetKeys, + createKey: mockCreateKey, + tasks: { + waitForTask: mockWaitForTask, + getTask: mockGetTask, + }, + }; + return { client, adminClient }; +}; + +describe("SearchManager", () => { + beforeEach(() => { + mockGetKeys.mockClear(); + mockCreateKey.mockClear(); + mockGetIndexes.mockClear(); + mockIndexGetSettings.mockClear(); + mockIndexResetSettings.mockClear(); + mockIndexUpdateSettings.mockClear(); + mockWaitForTask.mockClear(); + mockGetTask.mockClear(); + }); + + describe("create with custom clients", () => { + test("creates instance with provided clients", async () => { + const { client, adminClient } = createMockClient(); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + expect(manager).toBeDefined(); + }); + }); + + describe("getLocalizedIndexesOfEntity", () => { + test("returns indexes matching entity name", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [ + createMockIndex("song_zh-CN"), + createMockIndex("song_en"), + createMockIndex("artist_zh-CN"), + createMockIndex("no-locale"), + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const indexes = await manager.getLocalizedIndexesOfEntity("song"); + + expect(indexes).toEqual(["song_zh-CN", "song_en"]); + }); + + test("returns empty array when no matching indexes", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [createMockIndex("artist_zh-CN")], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const indexes = await manager.getLocalizedIndexesOfEntity("song"); + + expect(indexes).toEqual([]); + }); + }); + + describe("syncAllSettings", () => { + test("syncs settings when they differ", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [ + createMockIndex("song_zh-CN", { + searchableAttributes: ["old"], + filterableAttributes: [], + sortableAttributes: [], + rankingRules: [], + }), + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.syncAllSettings(); + + expect(mockIndexResetSettings).toHaveBeenCalled(); + expect(mockIndexUpdateSettings).toHaveBeenCalled(); + }); + + test("skips sync when settings match", async () => { + const { client, adminClient } = createMockClient(); + const matchingSettings = { + searchableAttributes: [ + "name", + "lyrics", + "description", + "singers", + "artists", + "bilibiliAid", + "bilibiliBvid", + ], + filterableAttributes: ["type", "tags", "singers", "engine"], + sortableAttributes: ["publishedAt", "bilibiliViews"], + rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], + embedders: { + "potion-multilingual-128M": { + source: "userProvided", + dimensions: 256, + }, + }, + }; + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [createMockIndex("song_zh-CN", matchingSettings)], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.syncAllSettings(); + + expect(mockIndexResetSettings).not.toHaveBeenCalled(); + expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); + }); + + test("skips indexes without locale suffix", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [createMockIndex("song")], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.syncAllSettings(); + + expect(mockIndexResetSettings).not.toHaveBeenCalled(); + expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); + }); + + test("skips indexes not in INDEX_SETTINGS", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [createMockIndex("unknown_zh-CN")], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.syncAllSettings(); + + expect(mockIndexResetSettings).not.toHaveBeenCalled(); + expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); + }); + }); + + describe("getAdminIndex", () => { + test("returns admin index instance", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const index = await manager.getAdminIndex("song_zh-CN"); + + expect(index).toBeDefined(); + }); + }); + + describe("getSearchIndex", () => { + test("returns search index instance", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const index = await manager.getSearchIndex("song_zh-CN"); + + expect(index).toBeDefined(); + }); + }); + + describe("listIndexes", () => { + test("returns list of all indexes", async () => { + const { client, adminClient } = createMockClient(); + const mockIndexes = [ + createMockIndex("song_zh-CN"), + createMockIndex("song_en"), + ] as unknown as Index[]; + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ results: mockIndexes }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const indexes = await manager.listIndexes(); + + expect(indexes.results).toEqual(mockIndexes); + }); + }); + + describe("waitForTask", () => { + test("waits for task completion", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + const mockTask = { uid: 123, status: "succeeded" } as Task; + mockWaitForTask.mockResolvedValue(mockTask); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const task = await manager.waitForTask(123); + + expect(task).toEqual(mockTask); + expect(mockWaitForTask).toHaveBeenCalledWith(123, { + timeout: 10000, + interval: 100, + }); + }); + }); + + describe("getTask", () => { + test("returns task details", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + const mockTask = { uid: 123, status: "succeeded" } as Task; + mockGetTask.mockResolvedValue(mockTask); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const task = await manager.getTask(123); + + expect(task).toEqual(mockTask); + }); + }); +}); diff --git a/packages/core/tests/unit/SongSearchService.test.ts b/packages/core/tests/unit/SongSearchService.test.ts new file mode 100644 index 0000000..3da6704 --- /dev/null +++ b/packages/core/tests/unit/SongSearchService.test.ts @@ -0,0 +1,413 @@ +import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; +import type { SongDetailsResponseDto } from "../../src/modules"; +import type { IRepositoryWithGetDetails } from "../../src/types/repository"; + +// Mock SearchManager +const mockGetLocalizedIndexesOfEntity = mock(); +const mockGetAdminIndex = mock(); +const mockGetSearchIndex = mock(); + +const mockAdminIndex = { + deleteDocument: mock(), + addDocuments: mock(), + search: mock(), +}; + +const mockSearchIndex = { + search: mock(), +}; + +const mockSearchManager = { + getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, + getAdminIndex: mockGetAdminIndex, + getSearchIndex: mockGetSearchIndex, + waitForTask: mock().mockResolvedValue(undefined), +}; + +// Mock EmbeddingAppApi +const mockEmbeddingsPost = mock(); +const mockEmbeddingManager = { + embeddings: { + post: mockEmbeddingsPost, + }, +}; + +// Import after mocking +const { SongSearchService } = await import("../../src/search/catalog/song"); + +const mockSongDetails: SongDetailsResponseDto = { + id: 1, + type: "ORIGINAL", + name: "Test Song", + language: "zh", + duration: 180, + description: "A test song", + coverUrl: "https://example.com/cover.jpg", + publishedAt: new Date("2024-01-01").toISOString(), + deletedAt: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + originalSongId: null, + localizedNames: { en: "Test Song EN", ja: "テストソング" }, + localizedDescriptions: { en: "A test song in English" }, + bilibiliAid: null, + bilibiliBvid: null, + vocadbId: null, + vcpediaId: null, + moegirlId: null, + singers: [ + { + id: 1, + name: "Singer 1", + language: "zh", + localizedNames: { en: "Singer 1 EN" }, + engine: "SynthV", + description: null, + localizedDescriptions: null, + createdAt: "", + updatedAt: "", + avatarUrl: null, + }, + ], + artists: [ + { + id: 1, + name: "Artist 1", + language: "zh", + localizedNames: { en: "Artist 1 EN" }, + description: null, + localizedDescriptions: null, + createdAt: "", + updatedAt: "", + aliases: [], + userId: null, + role: { + id: 1, + name: "", + localizedNames: null, + createdAt: "", + updatedAt: "", + deletedAt: null, + }, + }, + ], + lyrics: [ + { + language: "zh", + plainText: "歌词内容", + isTranslated: false, + }, + { + language: "en", + plainText: "Lyrics content", + isTranslated: true, + }, + ], +}; + +describe("SongSearchService", () => { + let service: InstanceType; + let mockRepository: IRepositoryWithGetDetails; + + beforeEach(() => { + mockGetLocalizedIndexesOfEntity.mockClear(); + mockGetAdminIndex.mockClear(); + mockGetSearchIndex.mockClear(); + mockAdminIndex.deleteDocument.mockClear(); + mockAdminIndex.addDocuments.mockClear(); + mockAdminIndex.search.mockClear(); + mockSearchIndex.search.mockClear(); + mockEmbeddingsPost.mockClear(); + + mockGetAdminIndex.mockResolvedValue(mockAdminIndex); + mockGetSearchIndex.mockResolvedValue(mockSearchIndex); + mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); + mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); + mockEmbeddingsPost.mockResolvedValue({ + data: { embeddings: [[0.1, 0.2, 0.3]] }, + }); + + mockRepository = { + getDetailsById: mock(), + }; + + service = new SongSearchService( + mockRepository, + mockSearchManager as never, + mockEmbeddingManager as never + ); + }); + + afterAll(() => { + mock.restore(); + mock.clearAllMocks(); + }); + + describe("sync", () => { + test("syncs song to all language indexes", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + expect(callArgs[1]).toEqual({ primaryKey: "id" }); + }); + + test("deletes document from all indexes when song not found", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); + mockGetLocalizedIndexesOfEntity.mockResolvedValue(["song_zh", "song_en"]); + + await service.sync(999); + + expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("song"); + expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); + }); + + test("handles missing search manager gracefully", async () => { + const serviceWithoutManager = new SongSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await serviceWithoutManager.sync(1); + + expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); + }); + + test("builds document with localized content", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + expect(callArgs).toBeDefined(); + const docs = callArgs[0] as { id: number; name: string }[]; + expect(docs[0].id).toBe(1); + expect(docs[0].name).toBe("Test Song EN"); + }); + + test("handles song with no localized content", async () => { + const songWithoutLocalization = { + ...mockSongDetails, + localizedNames: {}, + localizedDescriptions: {}, + singers: [{ ...mockSongDetails.singers[0], localizedNames: {} }], + artists: [{ ...mockSongDetails.artists[0], localizedNames: {} }], + }; + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + songWithoutLocalization + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + }); + + test("handles embedding generation failure", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + mockEmbeddingsPost.mockResolvedValue({ + data: null, + }); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const doc = mockAdminIndex.addDocuments.mock.calls[0][0][0]; + expect(doc._vectors).toBeUndefined(); + }); + }); + + describe("search", () => { + test("performs hybrid search with embedding", async () => { + const mockSearchResult = { + hits: [{ id: 1, name: "Test Song" }], + query: "test", + processingTimeMs: 30, + offset: 1, + limit: 1, + estimatedTotalHits: 1, + }; + mockSearchIndex.search.mockResolvedValue(mockSearchResult); + + const result = await service.search("test query", "zh"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("song_zh"); + expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); + expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { + vector: [0.1, 0.2, 0.3], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + expect(result).toEqual(mockSearchResult); + }); + + test("uses default language when not specified", async () => { + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("song_zh"); + }); + + test("throws when search manager not available", async () => { + const serviceWithoutManager = new SongSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + expect(serviceWithoutManager.search("test")).rejects.toThrow( + "Search or embedding service not available" + ); + }); + + test("handles missing embedding response gracefully", async () => { + mockEmbeddingsPost.mockResolvedValue({ data: null }); + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test", "zh"); + + expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { + vector: undefined, + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + }); + }); + + describe("getDocument (via sync)", () => { + test("uses correct lyrics for language", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + expect(callArgs).toBeDefined(); + const docs = callArgs[0] as { lyrics?: string }[]; + expect(docs[0].lyrics).toBeDefined(); + }); + + test("uses correct description for language", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const allCalls = mockAdminIndex.addDocuments.mock.calls; + const zhCall = allCalls.find((call: unknown[]) => { + const docs = call[0] as { description?: string }[]; + return docs[0].description === "A test song"; + }); + expect(zhCall).toBeDefined(); + }); + + test("uses localized description when available", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callsCount = mockAdminIndex.addDocuments.mock.calls.length; + expect(callsCount).toBeGreaterThan(0); + }); + + test("extracts localized singer names", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + const docs = callArgs[0] as { singers?: string[] }[]; + expect(docs[0].singers).toBeDefined(); + }); + + test("extracts localized artist names", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + const docs = callArgs[0] as { artists?: string[] }[]; + expect(docs[0].artists).toBeDefined(); + }); + + test("extracts engine from singers", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + const docs = callArgs[0] as { engine?: string[] }[]; + expect(docs[0].engine).toContain("SynthV"); + }); + + test("converts publishedAt to timestamp", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSongDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + const docs = callArgs[0] as { publishedAt?: number }[]; + expect(docs[0].publishedAt).toBeTypeOf("number"); + expect(docs[0].publishedAt).toBe(new Date("2024-01-01").getTime()); + }); + + test("handles null publishedAt", async () => { + const songWithoutDate = { + ...mockSongDetails, + publishedAt: null, + }; + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + songWithoutDate + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; + const docs = callArgs[0] as { publishedAt?: number }[]; + expect(docs[0].publishedAt).toBeUndefined(); + }); + }); +}); diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index bb8f5ae..f2f142b 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -1,8 +1,6 @@ import { describe, expect, mock, test } from "bun:test"; import { SongService, AppError } from "@cvsa/core/internal"; import type { SongDetailsResponseDto, ISongRepository, SongSearchService } from "@cvsa/core"; -import type { Serialized } from "@cvsa/db"; -import type { Song } from "@cvsa/db"; import { createMockRepository } from "../utils"; const mockSongDetails: SongDetailsResponseDto = { @@ -32,15 +30,27 @@ const mockSongDetails: SongDetailsResponseDto = { describe("SongService", () => { const mockRepository = createMockRepository({ - getById: async () => null, - getDetailsById: async () => mockSongDetails, - create: async () => Promise.resolve(null as unknown as Serialized), - update: async () => Promise.resolve(null as unknown as Serialized), + getById: async (id: number) => { + if (id === 1) { + return mockSongDetails; + } + return null; + }, + getDetailsById: async (id: number) => { + if (id === 1) { + return mockSongDetails; + } + return null; + }, + create: async () => mockSongDetails, + update: async () => mockSongDetails, softDelete: async () => {}, }); + const mockSearchService = { sync: mock(async (_id: number) => {}), }; + const songService = new SongService( mockRepository as unknown as ISongRepository, mockSearchService as unknown as SongSearchService @@ -65,4 +75,110 @@ describe("SongService", () => { }); }); }); + + describe("create", () => { + const createInput = { + name: "New Song", + type: "ORIGINAL" as const, + duration: 200, + }; + + test("creates song and calls search.sync", async () => { + const result = await songService.create(createInput); + + expect(result).toMatchObject({ + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput); + }); + + test("create song even when search.sync throws", async () => { + mockSearchService.sync.mockImplementationOnce(async () => { + throw new Error("Search service unavailable"); + }); + + const result = await songService.create(createInput); + + expect(result).toMatchObject({ + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput); + }); + }); + + describe("update", () => { + const updateInput = { name: "Updated Song" }; + + test("updates song and calls search.sync on success", async () => { + const result = await songService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.update(999, updateInput)).rejects.toThrow(AppError); + expect(songService.update(999, updateInput)).rejects.toThrow("error.song.notfound"); + expect(songService.update(999, updateInput)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + + test("updates song even when search.sync throws", async () => { + mockSearchService.sync.mockImplementationOnce(async () => { + throw new Error("Search service unavailable"); + }); + + const result = await songService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Test Song", + type: "ORIGINAL", + duration: 180, + }); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); + }); + }); + + describe("delete", () => { + test("soft deletes song and calls search.sync on success", async () => { + await songService.delete(1); + + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.delete(999)).rejects.toThrow(AppError); + expect(songService.delete(999)).rejects.toThrow("error.song.notfound"); + expect(songService.delete(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + + test("deletes song even when search.sync throws", async () => { + mockSearchService.sync.mockImplementationOnce(async () => { + throw new Error("Search service unavailable"); + }); + + await songService.delete(1); + + expect(mockRepository.softDelete).toHaveBeenCalledWith(1); + }); + }); }); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index bde370c..e94ca9a 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -11,3 +11,5 @@ export function createMockRepository(template: T) { return mockRepo; } + +export const OriginalMeiliSearch = await import("meilisearch"); diff --git a/packages/db/package.json b/packages/db/package.json index b2f8020..b6978db 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,7 +10,8 @@ "push": "bunx --bun prisma db push", "db:deploy": "bunx --bun prisma migrate deploy", "test:setup": "NODE_ENV=test bunx --bun prisma db push --accept-data-loss", - "studio": "bunx --bun prisma studio" + "studio": "bunx --bun prisma studio", + "reset": "bunx --bun prisma migrate reset" }, "devDependencies": { "@cvsa/prisma-zod": "^3.6.0", diff --git a/packages/db/prisma/models/core/artist.prisma b/packages/db/prisma/models/core/artist.prisma index f691a40..0cbdc4e 100644 --- a/packages/db/prisma/models/core/artist.prisma +++ b/packages/db/prisma/models/core/artist.prisma @@ -3,7 +3,7 @@ model Artist { name String? /// [LocalizedField] localizedNames Json? @map("localized_names") - language String @default("zh") + language String @default("zh") aliases String[] description String? /// [LocalizedField] diff --git a/packages/db/prisma/models/core/artistRole.prisma b/packages/db/prisma/models/core/artistRole.prisma index 6aa38eb..1b97f64 100644 --- a/packages/db/prisma/models/core/artistRole.prisma +++ b/packages/db/prisma/models/core/artistRole.prisma @@ -1,8 +1,8 @@ model ArtistRole { id Int @id @default(autoincrement()) - role String + name String /// [LocalizedField] - localizedRoles Json? @map("localized_roles") + localizedNames Json? @map("localized_names") creations Creation[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/packages/db/prisma/models/core/singer.prisma b/packages/db/prisma/models/core/singer.prisma index b876ff9..eb4185f 100644 --- a/packages/db/prisma/models/core/singer.prisma +++ b/packages/db/prisma/models/core/singer.prisma @@ -2,7 +2,7 @@ model Singer { id Int @id @default(autoincrement()) name String? avatarUrl String? @map("avatar_url") - language String @default("zh") + language String @default("zh") /// [LocalizedField] localizedNames Json? @map("localized_names") description String? diff --git a/packages/logger/package.json b/packages/logger/package.json index 4125126..62ee428 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -16,14 +16,7 @@ "@cvsa/observability": "workspace:*", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.214.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", - "@opentelemetry/resources": "^2.6.1", - "@opentelemetry/sdk-logs": "^0.214.0", - "@opentelemetry/sdk-node": "^0.214.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@t3-oss/env-core": "^0.13.11", "pino": "^10.3.1", - "pino-pretty": "^13.1.3", - "zod": "^4.3.6" + "pino-pretty": "^13.1.3" } } diff --git a/packages/logger/src/pinoSetup.ts b/packages/logger/src/pinoSetup.ts index 4d2aae0..7c15274 100644 --- a/packages/logger/src/pinoSetup.ts +++ b/packages/logger/src/pinoSetup.ts @@ -1,7 +1,7 @@ import { env } from "@cvsa/env"; import pino, { stdTimeFunctions } from "pino"; -function createPinoLogger(env: { LOG_LEVEL: string; NODE_ENV: string | undefined }): pino.Logger { +function createPinoLogger(): pino.Logger { const isProduction = env.NODE_ENV === "production"; const isTest = env.NODE_ENV === "test"; @@ -43,7 +43,5 @@ function createPinoLogger(env: { LOG_LEVEL: string; NODE_ENV: string | undefined }); } -export const pinoLogger = createPinoLogger({ - LOG_LEVEL: env.LOG_LEVEL, - NODE_ENV: env.NODE_ENV, -}); +export const pinoLogger = createPinoLogger(); +export { pino }; From 1396eabba4da20223dab61b583cdbd7631ee2b24 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 12 Apr 2026 18:53:33 +0800 Subject: [PATCH 14/37] feat(song): add song lyrics CRUD endpoints (#46) Implement full CRUD operations for song lyrics: - GET /v2/song/:id/lyrics - List all lyrics for a song - GET /v2/song/:id/lyric/:lyricId - Get specific lyric - POST /v2/song/:id/lyric - Create lyric (auth required) - PATCH /v2/song/:id/lyric/:lyricId - Update lyric (auth required) - DELETE /v2/song/:id/lyric/:lyricId - Delete lyric (auth required) Includes repository methods, service layer, DTOs, E2E tests, and i18n translations. --- AGENTS.md | 13 +- apps/backend/bunfig.toml | 2 + apps/backend/package.json | 4 +- .../backend/src/common/error/ConflictError.ts | 12 - apps/backend/src/common/error/index.ts | 1 - apps/backend/src/errorHandler.ts | 6 - .../src/handlers/catalog/song/create.ts | 10 +- .../src/handlers/catalog/song/index.ts | 4 +- .../handlers/catalog/song/lyrics/create.ts | 38 ++ .../handlers/catalog/song/lyrics/delete.ts | 34 ++ .../src/handlers/catalog/song/lyrics/get.ts | 29 + .../src/handlers/catalog/song/lyrics/index.ts | 13 + .../src/handlers/catalog/song/lyrics/list.ts | 28 + .../handlers/catalog/song/lyrics/update.ts | 39 ++ .../src/handlers/catalog/song/update.ts | 10 +- apps/backend/tests/auth.test.ts | 2 +- apps/backend/tests/songLyrics.test.ts | 564 ++++++++++++++++++ locale/modules/backend/messages/en.json | 1 + locale/modules/backend/messages/zh.json | 1 + packages/core/src/modules/catalog/song/dto.ts | 74 ++- .../catalog/song/repository.interface.ts | 26 +- .../src/modules/catalog/song/repository.ts | 99 ++- .../core/src/modules/catalog/song/service.ts | 79 ++- .../tests/integration/SongRepository.test.ts | 194 +++++- .../core/tests/unit/SongSearchService.test.ts | 13 +- packages/core/tests/unit/SongService.test.ts | 188 +++++- 26 files changed, 1391 insertions(+), 93 deletions(-) delete mode 100644 apps/backend/src/common/error/ConflictError.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/create.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/delete.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/get.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/index.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/list.ts create mode 100644 apps/backend/src/handlers/catalog/song/lyrics/update.ts create mode 100644 apps/backend/tests/songLyrics.test.ts diff --git a/AGENTS.md b/AGENTS.md index 42dd3f8..45cfbf8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,10 @@ - `apps/frontend`: Web interface (Not yet implemented) - `packages/db`: Prisma schema and generated database client - `packages/core`: Shared business logic, DTOs, and Zod schemas +- `packages/embedding`: A standalone embedding REST service +- `packages/logger`: The application logger setup +- `packages/observability`: Observability configurations +- `packages/env`: Package for managing environment variables ## 2. Core Mental Model & Architecture @@ -68,17 +72,10 @@ These rules are absolute. Violation will cause pipeline or runtime failures. - **camelCase**: Functions (`getUserById`), Variables, and standard file names (`authHandler.ts`). - **UPPER_SNAKE_CASE**: Constants (`MAX_RATE_LIMIT`). -### Import Hierarchy -Order imports from farthest to closest. Prioritize path aliases -1. External: Built-in and external NPM packages. -2. Workspace: Internal packages (`@cvsa/core`, `@cvsa/db`). -3. Aliases: `@/*`, `@modules/*`, `@common/*` pointing to `./src/`. -4. **[PROHIBITED]**: relative paths (`../`, `./`, `../../../`, etc.) - ### Module-Specific Information - **Backend**: 1. Our endpoints starts with `/v2` and this prefix is already configured in root handler. In all of the rest handlers, always use the complete path **without** the version prefix (e.g. `/song/:id/details`) - 2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful style. + 2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful pattern. ## 5. Execution Commands & Testing diff --git a/apps/backend/bunfig.toml b/apps/backend/bunfig.toml index 3fc94a4..4856355 100644 --- a/apps/backend/bunfig.toml +++ b/apps/backend/bunfig.toml @@ -10,4 +10,6 @@ coveragePathIgnorePatterns = [ "src/utils/randomId.ts", "src/types/**", "**/observability/src/**", + "**/packages/core/**", + "src/*.ts" ] \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 4f0f331..c0e1eb8 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -6,8 +6,8 @@ "lint": "bunx --bun biome lint", "dev": "NODE_ENV=development bun --watch src/index.ts", "start": "bun src/index.ts", - "test": "NODE_ENV=test bun test", - "test:coverage": "NODE_ENV=test bun test --coverage", + "test": "bun test --concurrent", + "test:coverage": "bun test --coverage --concurrent", "typecheck": "bunx --bun tsc --noEmit" }, "devDependencies": { diff --git a/apps/backend/src/common/error/ConflictError.ts b/apps/backend/src/common/error/ConflictError.ts deleted file mode 100644 index d93345e..0000000 --- a/apps/backend/src/common/error/ConflictError.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AppError } from "@cvsa/core"; - -export class ConflictError extends AppError { - constructor( - field: string, - code: string = "APP_CONFLICT_ERROR", - message = "Record already exists", - statusCode: number = 409 - ) { - super(message, code, statusCode, { field }); - } -} diff --git a/apps/backend/src/common/error/index.ts b/apps/backend/src/common/error/index.ts index 0bddc90..14ae090 100644 --- a/apps/backend/src/common/error/index.ts +++ b/apps/backend/src/common/error/index.ts @@ -1,3 +1,2 @@ -export * from "./ConflictError"; export * from "./RateLimitError"; export * from "./getErrorResponse"; diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts index f1600ec..69728ef 100644 --- a/apps/backend/src/errorHandler.ts +++ b/apps/backend/src/errorHandler.ts @@ -8,11 +8,6 @@ import { i18nMiddleware } from "@/middlewares"; import { ip } from "elysia-ip"; import { formatGinLog, getLogLevelForRequest } from "@common/log"; -const AUTH_CONFLICT_CODES = [ - "USERNAME_IS_ALREADY_TAKEN", - "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", -] as const; - const AUTH_INVALID_CODES = [ "INVALID_CREDENTIALS", "INVALID_EMAIL_OR_PASSWORD", @@ -20,7 +15,6 @@ const AUTH_INVALID_CODES = [ "EMAIL_NOT_VERIFIED", ] as const; -type AuthConflictCode = (typeof AUTH_CONFLICT_CODES)[number]; type AuthInvalidCode = (typeof AUTH_INVALID_CODES)[number]; interface ErrorStoreData { diff --git a/apps/backend/src/handlers/catalog/song/create.ts b/apps/backend/src/handlers/catalog/song/create.ts index 23ca1fb..b5fc7e1 100644 --- a/apps/backend/src/handlers/catalog/song/create.ts +++ b/apps/backend/src/handlers/catalog/song/create.ts @@ -1,7 +1,11 @@ import { Elysia } from "elysia"; -import { CreateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; +import { + CreateSongRequestSchema, + ErrorResponseSchema, + SongResponseSchema, + songService, +} from "@cvsa/core"; import { authMiddleware } from "@/middlewares"; -import { SongSchema } from "@cvsa/db"; import { traceTask } from "@/common/trace"; export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( @@ -19,7 +23,7 @@ export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(a description: "Create a new song entry in the catalog. Requires authentication.", }, response: { - 201: SongSchema, + 201: SongResponseSchema, 401: ErrorResponseSchema, }, } diff --git a/apps/backend/src/handlers/catalog/song/index.ts b/apps/backend/src/handlers/catalog/song/index.ts index adfa5af..dc9e11c 100644 --- a/apps/backend/src/handlers/catalog/song/index.ts +++ b/apps/backend/src/handlers/catalog/song/index.ts @@ -4,10 +4,12 @@ import { songUpdateHandler } from "./update"; import { songDeleteHandler } from "./delete"; import { songDetailsHandler } from "./details"; import { songSearchHandler } from "./search"; +import { songLyricsHandler } from "./lyrics"; export const songHandler = new Elysia({ name: "songHandler" }) .use(songDetailsHandler) .use(songCreateHandler) .use(songUpdateHandler) .use(songDeleteHandler) - .use(songSearchHandler); + .use(songSearchHandler) + .use(songLyricsHandler); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/create.ts b/apps/backend/src/handlers/catalog/song/lyrics/create.ts new file mode 100644 index 0000000..33573ea --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/create.ts @@ -0,0 +1,38 @@ +import { Elysia } from "elysia"; +import { + ErrorResponseSchema, + SongLyricsCreateRequestSchema, + SongLyricsResponseSchema, + songService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; +import z from "zod"; + +export const songLyricsCreateHandler = new Elysia({ name: "songLyricsCreateHandler" }) + .use(authMiddleware) + .post( + "/song/:id/lyric", + async ({ body, params, status }) => { + const lyric = await traceTask("songService.createLyric", async () => { + return await songService.createLyric(params.id, body); + }); + return status(201, lyric); + }, + { + body: SongLyricsCreateRequestSchema, + detail: { + summary: "Create Song Lyric", + description: "Add a new lyrics item for a song. Requires authentication.", + }, + response: { + 201: SongLyricsResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } + ); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/delete.ts b/apps/backend/src/handlers/catalog/song/lyrics/delete.ts new file mode 100644 index 0000000..126e19f --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/delete.ts @@ -0,0 +1,34 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, songService } from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; +import z from "zod"; + +export const songLyricsDeleteHandler = new Elysia({ name: "songLyricsDeleteHandler" }) + .use(authMiddleware) + .delete( + "/song/:id/lyric/:lyricId", + async ({ params, set }) => { + await traceTask("songService.deleteLyric", async () => { + return await songService.deleteLyric(params.id, params.lyricId); + }); + set.status = 204; + return null; + }, + { + detail: { + summary: "Delete Song Lyric", + description: "Delete a specific lyrics item for a song. Requires authentication.", + }, + response: { + 204: z.null(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + lyricId: z.coerce.number().int().positive(), + }), + } + ); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/get.ts b/apps/backend/src/handlers/catalog/song/lyrics/get.ts new file mode 100644 index 0000000..97d6a39 --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/get.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, SongLyricsResponseSchema, songService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const songLyricsGetHandler = new Elysia().get( + "/song/:id/lyric/:lyricId", + async ({ params, status }) => { + const lyric = await traceTask("songService.getLyric", async () => { + return await songService.getLyric(params.id, params.lyricId); + }); + return status(200, lyric); + }, + { + detail: { + summary: "Get Song Lyric", + description: "Retrieve a specific lyrics item for a song by its ID.", + }, + response: { + 200: SongLyricsResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + lyricId: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/index.ts b/apps/backend/src/handlers/catalog/song/lyrics/index.ts new file mode 100644 index 0000000..21b4a7e --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/index.ts @@ -0,0 +1,13 @@ +import { Elysia } from "elysia"; +import { songLyricsListHandler } from "./list"; +import { songLyricsGetHandler } from "./get"; +import { songLyricsCreateHandler } from "./create"; +import { songLyricsUpdateHandler } from "./update"; +import { songLyricsDeleteHandler } from "./delete"; + +export const songLyricsHandler = new Elysia({ name: "songLyricsHandler" }) + .use(songLyricsListHandler) + .use(songLyricsGetHandler) + .use(songLyricsCreateHandler) + .use(songLyricsUpdateHandler) + .use(songLyricsDeleteHandler); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/list.ts b/apps/backend/src/handlers/catalog/song/lyrics/list.ts new file mode 100644 index 0000000..3444506 --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/list.ts @@ -0,0 +1,28 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, SongLyricsListResponseSchema, songService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const songLyricsListHandler = new Elysia().get( + "/song/:id/lyrics", + async ({ params, status }) => { + const lyrics = await traceTask("songService.listLyrics", async () => { + return await songService.listLyrics(params.id); + }); + return status(200, lyrics); + }, + { + detail: { + summary: "List Song Lyrics", + description: "Retrieve all available lyrics for a specific song by its ID.", + }, + response: { + 200: SongLyricsListResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/update.ts b/apps/backend/src/handlers/catalog/song/lyrics/update.ts new file mode 100644 index 0000000..3d0f6ce --- /dev/null +++ b/apps/backend/src/handlers/catalog/song/lyrics/update.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { + ErrorResponseSchema, + SongLyricsUpdateRequestSchema, + SongLyricsResponseSchema, + songService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; +import z from "zod"; + +export const songLyricsUpdateHandler = new Elysia({ name: "songLyricsUpdateHandler" }) + .use(authMiddleware) + .patch( + "/song/:id/lyric/:lyricId", + async ({ body, params, status }) => { + const lyric = await traceTask("songService.updateLyric", async () => { + return await songService.updateLyric(params.id, params.lyricId, body); + }); + return status(200, lyric); + }, + { + body: SongLyricsUpdateRequestSchema, + detail: { + summary: "Update Song Lyric", + description: "Update a specific lyrics item for a song. Requires authentication.", + }, + response: { + 200: SongLyricsResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + lyricId: z.coerce.number().int().positive(), + }), + } + ); diff --git a/apps/backend/src/handlers/catalog/song/update.ts b/apps/backend/src/handlers/catalog/song/update.ts index 0aa6bcb..34c723b 100644 --- a/apps/backend/src/handlers/catalog/song/update.ts +++ b/apps/backend/src/handlers/catalog/song/update.ts @@ -1,8 +1,12 @@ import { Elysia } from "elysia"; import { z } from "zod"; -import { UpdateSongRequestSchema, ErrorResponseSchema, songService } from "@cvsa/core"; +import { + UpdateSongRequestSchema, + ErrorResponseSchema, + songService, + SongResponseSchema, +} from "@cvsa/core"; import { authMiddleware } from "@/middlewares"; -import { SongSchema } from "@cvsa/db"; import { traceTask } from "@/common/trace"; export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }) @@ -26,7 +30,7 @@ export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }) "Update metadata of an existing song identified by its ID. Requires authentication.", }, response: { - 200: SongSchema, + 200: SongResponseSchema, 400: ErrorResponseSchema, 401: ErrorResponseSchema, 404: ErrorResponseSchema, diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index 20fab02..6a9bb96 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; diff --git a/apps/backend/tests/songLyrics.test.ts b/apps/backend/tests/songLyrics.test.ts new file mode 100644 index 0000000..f2eb306 --- /dev/null +++ b/apps/backend/tests/songLyrics.test.ts @@ -0,0 +1,564 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@cvsa/db"; + +const api = treaty(app); + +describe("Song Lyrics E2E Tests", () => { + beforeAll(async () => { + await prisma.$connect(); + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.lyrics.deleteMany(); + await prisma.song.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.lyrics.deleteMany(); + await prisma.song.deleteMany(); + await prisma.$disconnect(); + }); + + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: `${Math.random()}`, + password: "password123", + }); + return signup.data?.data.token; + } + + describe("GET /v2/song/:id/lyrics - List Song Lyrics", () => { + test("should return empty array when song has no lyrics", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); + + expect(status).toBe(200); + expect(data).toEqual([]); + }); + + test("should return all lyrics for a song", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "第一行歌词", + }, + { + language: "en", + isTranslated: true, + plainText: "First line lyrics", + }, + ], + }, + }, + }); + + const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); + + expect(status).toBe(200); + expect(data).toHaveLength(2); + expect(data?.[0]).toMatchObject({ + language: expect.any(String), + isTranslated: expect.any(Boolean), + }); + }); + + test("should return 404 for nonexistent song", async () => { + const { error, status } = await api.v2.song({ id: 99999 }).lyrics.get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should not return deleted lyrics", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "第一行歌词", + deletedAt: new Date(), + }, + { + language: "en", + isTranslated: true, + plainText: "First line lyrics", + }, + ], + }, + }, + }); + + const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); + + expect(status).toBe(200); + expect(data).toHaveLength(1); + expect(data?.[0].language).toBe("en"); + }); + }); + + describe("GET /v2/song/:id/lyric/:lyricId - Get Specific Lyric", () => { + test("should return a specific lyric", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "第一行歌词", + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { data, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: lyrics.id, + language: "zh", + isTranslated: false, + plainText: "第一行歌词", + }); + }); + + test("should return 404 for nonexistent song", async () => { + const { error, status } = await api.v2.song({ id: 99999 }).lyric({ lyricId: 1 }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should return 404 for nonexistent lyric", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: 99999 }) + .get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should return 404 for deleted lyric", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "第一行歌词", + deletedAt: new Date(), + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("POST /v2/song/:id/lyric - Create Lyric", () => { + test("should create a lyric with authentication", async () => { + const token = await getAuthToken(); + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const payload = { + language: "ja", + isTranslated: true, + plainText: "日本語の歌詞", + lrc: "[00:00.00]日本語の歌詞", + ttml: "日本語の歌詞", + }; + + const { data, status } = await api.v2.song({ id: song.id }).lyric.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + language: "ja", + isTranslated: true, + plainText: "日本語の歌詞", + lrc: "[00:00.00]日本語の歌詞", + ttml: "日本語の歌詞", + }); + }); + + test("should return 401 without authentication", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const { error, status } = await api.v2.song({ id: song.id }).lyric.post({ + language: "zh", + plainText: "歌词", + }); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should return 404 for nonexistent song", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2.song({ id: 99999 }).lyric.post( + { + language: "zh", + plainText: "歌词", + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("PATCH /v2/song/:id/lyric/:lyricId - Update Lyric", () => { + test("should update a lyric with authentication", async () => { + const token = await getAuthToken(); + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "原歌词", + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { data, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .patch( + { + language: "en", + isTranslated: true, + plainText: "Updated lyrics", + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: lyrics.id, + language: "en", + isTranslated: true, + plainText: "Updated lyrics", + }); + }); + + test("should return 401 without authentication", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "歌词", + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .patch({ + plainText: "Updated lyrics", + }); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should return 404 for nonexistent song", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2 + .song({ id: 99999 }) + .lyric({ lyricId: 1 }) + .patch( + { + plainText: "Updated lyrics", + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should return 404 for nonexistent lyric", async () => { + const token = await getAuthToken(); + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: 99999 }) + .patch( + { + plainText: "Updated lyrics", + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("DELETE /v2/song/:id/lyric/:lyricId - Delete Lyric", () => { + test("should soft delete a lyric with authentication", async () => { + const token = await getAuthToken(); + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "歌词", + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedLyric = await prisma.lyrics.findUnique({ + where: { id: lyrics.id }, + }); + expect(deletedLyric?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + lyrics: { + create: [ + { + language: "zh", + isTranslated: false, + plainText: "歌词", + }, + ], + }, + }, + }); + + const lyrics = await prisma.lyrics.findFirst({ + where: { songId: song.id }, + }); + expect(lyrics).not.toBeNull(); + if (!lyrics) return; + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: lyrics.id }) + .delete({}, {}); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should return 404 for nonexistent song", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2 + .song({ id: 99999 }) + .lyric({ lyricId: 1 }) + .delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should return 404 for nonexistent lyric", async () => { + const token = await getAuthToken(); + const song = await prisma.song.create({ + data: { + name: "Test Song", + type: "ORIGINAL", + }, + }); + + const { error, status } = await api.v2 + .song({ id: song.id }) + .lyric({ lyricId: 99999 }) + .delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); +}); diff --git a/locale/modules/backend/messages/en.json b/locale/modules/backend/messages/en.json index 50e1538..65f64a0 100644 --- a/locale/modules/backend/messages/en.json +++ b/locale/modules/backend/messages/en.json @@ -1,6 +1,7 @@ { "error.unauthorized": "Unauthorized", "error.song.notfound": "This song does not exist.", + "error.lyric.notfound": "This lyric does not exist.", "error.signup.failed": "Unable to sign up. Please try again in a moment.", "error.login.failed": "Unable to log in. Please try again in a moment.", "error.not-found": "The requested resource was not found.", diff --git a/locale/modules/backend/messages/zh.json b/locale/modules/backend/messages/zh.json index d80d144..52acc79 100644 --- a/locale/modules/backend/messages/zh.json +++ b/locale/modules/backend/messages/zh.json @@ -1,6 +1,7 @@ { "error.unauthorized": "未登录", "error.song.notfound": "此歌曲不存在", + "error.lyric.notfound": "此歌词不存在", "error.signup.failed": "很抱歉,暂时无法注册", "error.login.failed": "很抱歉,暂时无法登录", "error.not-found": "请求的路径不存在", diff --git a/packages/core/src/modules/catalog/song/dto.ts b/packages/core/src/modules/catalog/song/dto.ts index 09fd5d4..6c1372d 100644 --- a/packages/core/src/modules/catalog/song/dto.ts +++ b/packages/core/src/modules/catalog/song/dto.ts @@ -6,6 +6,7 @@ import { ArtistRoleSchema, SongTypeSchema, type Serialized, + LyricsSchema, } from "@cvsa/db"; export type SongId = number; @@ -17,12 +18,20 @@ const CreateCreationSchema = z.object({ const CreatePerformanceSchema = z.object({ singerId: z.int().positive(), - voicebankId: z.int().positive().nullish(), - svsEngineId: z.int().positive().nullish(), - svsEngineVersionId: z.int().positive().nullish(), + voicebankId: z.int().positive().optional(), + svsEngineId: z.int().positive().optional(), + svsEngineVersionId: z.int().positive().optional(), }); -const CreateLyricsSchema = z.object({ +export const SongLyricsCreateRequestSchema = z.object({ + language: z.string().optional(), + isTranslated: z.boolean().optional(), + plainText: z.string().optional(), + ttml: z.string().optional(), + lrc: z.string().optional(), +}); + +export const SongLyricsUpdateRequestSchema = z.object({ language: z.string().optional(), isTranslated: z.boolean().optional(), plainText: z.string().optional(), @@ -31,31 +40,31 @@ const CreateLyricsSchema = z.object({ }); export const CreateSongRequestSchema = z.object({ - name: z.string().nullish(), - type: SongTypeSchema.nullish(), - language: z.string().nullish(), - localizedNames: z.record(z.string(), z.string()).nullish(), - description: z.string().nullish(), - localizedDescriptions: z.record(z.string(), z.string()).nullish(), - duration: z.int().nullish(), - coverUrl: z.url().nullish(), - publishedAt: z.iso.datetime().nullish(), + name: z.string().optional(), + type: SongTypeSchema.optional(), + language: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), + duration: z.int().optional(), + coverUrl: z.url().optional(), + publishedAt: z.iso.datetime().optional(), performances: z.array(CreatePerformanceSchema).optional(), creations: z.array(CreateCreationSchema).optional(), - lyrics: z.array(CreateLyricsSchema).optional(), + lyrics: z.array(SongLyricsCreateRequestSchema).optional(), }); export const UpdateSongRequestSchema = z.object({ - name: z.string().nullish(), - type: SongTypeSchema.nullish(), - duration: z.int().nullish(), - description: z.string().nullish(), - coverUrl: z.url().nullish(), - publishedAt: z.iso.datetime().nullish(), + name: z.string().optional(), + type: SongTypeSchema.optional(), + duration: z.int().optional(), + description: z.string().optional(), + coverUrl: z.url().optional(), + publishedAt: z.iso.datetime().optional(), }); export const SongDetailsResponseSchema = z.intersection( - SongSchema, + SongSchema.omit({ deletedAt: true }), z.object({ singers: z .intersection( @@ -71,16 +80,25 @@ export const SongDetailsResponseSchema = z.intersection( z.object({ role: ArtistRoleSchema }) ) .array(), - lyrics: z.array( - z.object({ - plainText: z.string().nullable(), - isTranslated: z.boolean(), - language: z.string().nullable(), - }) - ), + lyrics: LyricsSchema.omit({ deletedAt: true }).array(), }) ); +export const SongResponseSchema = SongSchema.omit({ deletedAt: true }); + +export const SongLyricsResponseSchema = LyricsSchema.omit({ + songId: true, + deletedAt: true, +}); + +export const SongLyricsListResponseSchema = SongLyricsResponseSchema.array(); + +export type SongResponseDto = Serialized>; export type CreateSongRequestDto = Serialized>; export type UpdateSongRequestDto = Serialized>; export type SongDetailsResponseDto = Serialized>; + +export type SongLyricsResponseDto = Serialized>; +export type SongLyricsListResponseDto = Serialized>; +export type SongLyricsCreateRequestDto = Serialized>; +export type SongLyricsUpdateRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/song/repository.interface.ts b/packages/core/src/modules/catalog/song/repository.interface.ts index f19022c..be9083f 100644 --- a/packages/core/src/modules/catalog/song/repository.interface.ts +++ b/packages/core/src/modules/catalog/song/repository.interface.ts @@ -1,20 +1,38 @@ -import type { Song, TxClient, Serialized } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; import type { CreateSongRequestDto, SongId, UpdateSongRequestDto, SongDetailsResponseDto, + SongResponseDto, + SongLyricsCreateRequestDto, + SongLyricsUpdateRequestDto, + SongLyricsResponseDto, + SongLyricsListResponseDto, } from "./dto"; export abstract class ISongRepository implements IRepositoryWithGetDetails { - abstract getById(id: SongId, tx?: TxClient): Promise | null>; + abstract getById(id: SongId, tx?: TxClient): Promise; abstract getDetailsById(id: SongId, tx?: TxClient): Promise; - abstract create(input: CreateSongRequestDto, tx?: TxClient): Promise>; + abstract create(input: CreateSongRequestDto, tx?: TxClient): Promise; abstract update( id: SongId, input: UpdateSongRequestDto, tx?: TxClient - ): Promise>; + ): Promise; abstract softDelete(id: SongId, tx?: TxClient): Promise; + abstract createLyrics( + id: SongId, + input: SongLyricsCreateRequestDto, + tx?: TxClient + ): Promise; + abstract getLyricsBySongId(id: SongId, tx?: TxClient): Promise; + abstract getLyricById(lyricId: number, tx?: TxClient): Promise; + abstract updateLyric( + lyricId: number, + input: SongLyricsUpdateRequestDto, + tx?: TxClient + ): Promise; + abstract softDeleteLyric(lyricId: number, tx?: TxClient): Promise; } diff --git a/packages/core/src/modules/catalog/song/repository.ts b/packages/core/src/modules/catalog/song/repository.ts index 0444a94..7ac77bc 100644 --- a/packages/core/src/modules/catalog/song/repository.ts +++ b/packages/core/src/modules/catalog/song/repository.ts @@ -4,6 +4,8 @@ import type { SongId, UpdateSongRequestDto, SongDetailsResponseDto, + SongLyricsCreateRequestDto, + SongLyricsUpdateRequestDto, } from "./dto"; import type { ISongRepository } from "./repository.interface"; import { transformPrismaResult, type TxClient } from "@cvsa/db"; @@ -14,7 +16,10 @@ export class SongRepository implements ISongRepository { async getById(id: SongId, tx?: TxClient) { const client = tx ?? this.prisma; return transformPrismaResult( - await client.song.findFirst({ where: { id, deletedAt: null } }) + await client.song.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) ); } @@ -37,7 +42,14 @@ export class SongRepository implements ISongRepository { artist: true, }, }, - lyrics: true, + lyrics: { + omit: { + deletedAt: true, + }, + }, + }, + omit: { + deletedAt: true, }, }) ); @@ -63,13 +75,7 @@ export class SongRepository implements ISongRepository { role: item.role, }; }), - lyrics: lyrics.map((item) => { - return { - plainText: item.plainText, - language: item.language, - isTranslated: item.isTranslated, - }; - }), + lyrics: lyrics, ...song, }; } @@ -81,15 +87,7 @@ export class SongRepository implements ISongRepository { return transformPrismaResult( await client.song.create({ data: { - language: songData.language ?? undefined, - type: songData.type ?? null, - name: songData.name ?? null, - localizedNames: songData.localizedNames ?? undefined, - localizedDescriptions: songData.localizedDescriptions ?? undefined, - duration: songData.duration ?? null, - description: songData.description ?? null, - coverUrl: songData.coverUrl ?? null, - publishedAt: songData.publishedAt ?? null, + ...songData, performances: performances && { create: performances, }, @@ -103,6 +101,9 @@ export class SongRepository implements ISongRepository { create: lyrics, }, }, + omit: { + deletedAt: true, + }, }) ); } @@ -114,6 +115,9 @@ export class SongRepository implements ISongRepository { await client.song.update({ where: { id }, data: input, + omit: { + deletedAt: true, + }, }) ); } @@ -122,4 +126,63 @@ export class SongRepository implements ISongRepository { const client = tx ?? this.prisma; await client.song.update({ where: { id }, data: { deletedAt: new Date() } }); } + + async createLyrics(id: SongId, input: SongLyricsCreateRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.lyrics.create({ + data: { + songId: id, + ...input, + }, + omit: { + deletedAt: true, + }, + }) + ); + } + + async getLyricsBySongId(id: SongId, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.lyrics.findMany({ + where: { songId: id, deletedAt: null }, + omit: { deletedAt: true, songId: true }, + }) + ); + } + + async getLyricById(lyricId: number, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.lyrics.findFirst({ + where: { id: lyricId, deletedAt: null }, + omit: { deletedAt: true, songId: true }, + }) + ); + } + + async updateLyric(lyricId: number, input: SongLyricsUpdateRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.lyrics.update({ + where: { id: lyricId }, + data: input, + omit: { deletedAt: true, songId: true }, + }) + ); + } + + async softDeleteLyric(lyricId: number, tx?: TxClient) { + const client = tx ?? this.prisma; + + await client.lyrics.update({ + where: { id: lyricId }, + data: { deletedAt: new Date() }, + }); + } } diff --git a/packages/core/src/modules/catalog/song/service.ts b/packages/core/src/modules/catalog/song/service.ts index 7a62940..ec58fa1 100644 --- a/packages/core/src/modules/catalog/song/service.ts +++ b/packages/core/src/modules/catalog/song/service.ts @@ -1,11 +1,15 @@ import type { SongSearchService } from "@cvsa/core/internal"; import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; -import type { Song, Serialized } from "@cvsa/db"; import type { SongDetailsResponseDto, SongId, CreateSongRequestDto, UpdateSongRequestDto, + SongResponseDto, + SongLyricsCreateRequestDto, + SongLyricsUpdateRequestDto, + SongLyricsResponseDto, + SongLyricsListResponseDto, } from "./dto"; import type { ISongRepository } from "./repository.interface"; import { traceTask } from "@cvsa/observability"; @@ -27,7 +31,7 @@ export class SongService implements IServiceWithGetDetails> { + async create(input: CreateSongRequestDto): Promise { const result = await traceTask("db create song", async () => { return await this.repository.create(input); }); @@ -39,7 +43,7 @@ export class SongService implements IServiceWithGetDetails> { + async update(id: SongId, input: UpdateSongRequestDto): Promise { const existing = await this.repository.getById(id); if (existing === null) { throw new AppError("error.song.notfound", "NOT_FOUND", 404); @@ -70,4 +74,73 @@ export class SongService implements IServiceWithGetDetails { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + return traceTask("db list lyrics", async () => { + return await this.repository.getLyricsBySongId(id); + }); + } + + async getLyric(id: SongId, lyricId: number): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + const lyric = await traceTask("db get lyric", async () => { + return await this.repository.getLyricById(lyricId); + }); + if (lyric === null) { + throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); + } + return lyric; + } + + async createLyric( + id: SongId, + input: SongLyricsCreateRequestDto + ): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + return traceTask("db create lyric", async () => { + return await this.repository.createLyrics(id, input); + }); + } + + async updateLyric( + id: SongId, + lyricId: number, + input: SongLyricsUpdateRequestDto + ): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + const lyric = await this.repository.getLyricById(lyricId); + if (lyric === null) { + throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); + } + return traceTask("db update lyric", async () => { + return await this.repository.updateLyric(lyricId, input); + }); + } + + async deleteLyric(id: SongId, lyricId: number): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + const lyric = await this.repository.getLyricById(lyricId); + if (lyric === null) { + throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); + } + await traceTask("db delete lyric", async () => { + return await this.repository.softDeleteLyric(lyricId); + }); + } } diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index ee146c3..395aedc 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -47,7 +47,8 @@ describe("SongRepository Integration Tests", () => { expect(result.duration).toBe(180); expect(result.description).toBe("A test song"); expect(result.coverUrl).toBe("https://example.com/cover.jpg"); - expect(result.deletedAt).toBeNull(); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); }); test("should create a song with minimal fields", async () => { @@ -73,6 +74,8 @@ describe("SongRepository Integration Tests", () => { expect(result).toBeDefined(); expect(result?.id).toBe(created.id); expect(result?.name).toBe("Find Me Song"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); }); test("should return null when song does not exist", async () => { @@ -134,6 +137,8 @@ describe("SongRepository Integration Tests", () => { name: "作曲", }, }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); }); test("should return null when song does not exist", async () => { @@ -157,6 +162,8 @@ describe("SongRepository Integration Tests", () => { expect(result.name).toBe("New Name"); expect(result.type).toBe("COVER"); expect(result.duration).toBe(200); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); }); test("should update only specified fields", async () => { @@ -184,4 +191,189 @@ describe("SongRepository Integration Tests", () => { expect(result).toBeNull(); }); }); + + describe("createLyrics", () => { + test("should create lyrics for a song", async () => { + const song = await repository.create({ name: "Song with Lyrics" }); + + const lyric = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "歌词内容", + }); + + expect(lyric).toBeDefined(); + expect(lyric.id).toBeGreaterThan(0); + expect(lyric.language).toBe("zh"); + expect(lyric.isTranslated).toBe(false); + expect(lyric.plainText).toBe("歌词内容"); + expect(lyric.ttml).toBeNull(); + expect(lyric.lrc).toBeNull(); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(lyric.deletedAt).not.toBeDefined(); + }); + + test("should create lyrics with optional fields", async () => { + const song = await repository.create({ name: "Song with Full Lyrics" }); + + const lyric = await repository.createLyrics(song.id, { + language: "ja", + isTranslated: true, + plainText: "日本語の歌詞", + ttml: "test", + lrc: "[00:00.00]test", + }); + + expect(lyric.language).toBe("ja"); + expect(lyric.isTranslated).toBe(true); + expect(lyric.plainText).toBe("日本語の歌詞"); + expect(lyric.ttml).toBe("test"); + expect(lyric.lrc).toBe("[00:00.00]test"); + }); + }); + + describe("getLyricsBySongId", () => { + test("should return all lyrics for a song", async () => { + const song = await repository.create({ + name: "Song with Multiple Lyrics", + }); + + await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "中文歌词", + }); + await repository.createLyrics(song.id, { + language: "en", + isTranslated: true, + plainText: "English lyrics", + }); + + const lyrics = await repository.getLyricsBySongId(song.id); + + expect(lyrics).toHaveLength(2); + expect(lyrics.map((l) => l.language)).toContain("zh"); + expect(lyrics.map((l) => l.language)).toContain("en"); + }); + + test("should return empty array when song has no lyrics", async () => { + const song = await repository.create({ name: "Song without Lyrics" }); + + const lyrics = await repository.getLyricsBySongId(song.id); + + expect(lyrics).toEqual([]); + }); + + test("should not return deleted lyrics", async () => { + const song = await repository.create({ name: "Song with Deleted Lyrics" }); + + const lyric = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "将被删除的歌词", + }); + await repository.softDeleteLyric(lyric.id); + + const lyrics = await repository.getLyricsBySongId(song.id); + + expect(lyrics).toHaveLength(0); + }); + }); + + describe("getLyricById", () => { + test("should return lyric when exists", async () => { + const song = await repository.create({ name: "Song" }); + const created = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "歌词", + }); + + const lyric = await repository.getLyricById(created.id); + + expect(lyric).toBeDefined(); + expect(lyric?.id).toBe(created.id); + expect(lyric?.plainText).toBe("歌词"); + }); + + test("should return null when lyric does not exist", async () => { + const lyric = await repository.getLyricById(999999); + + expect(lyric).toBeNull(); + }); + + test("should return null when lyric is deleted", async () => { + const song = await repository.create({ name: "Song" }); + const created = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "歌词", + }); + await repository.softDeleteLyric(created.id); + + const lyric = await repository.getLyricById(created.id); + + expect(lyric).toBeNull(); + }); + }); + + describe("updateLyric", () => { + test("should update all fields", async () => { + const song = await repository.create({ name: "Song" }); + const created = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "原歌词", + }); + + const updated = await repository.updateLyric(created.id, { + language: "en", + isTranslated: true, + plainText: "Updated lyrics", + ttml: "updated", + lrc: "[00:00.00]updated", + }); + + expect(updated.language).toBe("en"); + expect(updated.isTranslated).toBe(true); + expect(updated.plainText).toBe("Updated lyrics"); + expect(updated.ttml).toBe("updated"); + expect(updated.lrc).toBe("[00:00.00]updated"); + }); + + test("should update only specified fields", async () => { + const song = await repository.create({ name: "Song" }); + const created = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "原歌词", + ttml: "original", + }); + + const updated = await repository.updateLyric(created.id, { + plainText: "新歌词", + }); + + expect(updated.plainText).toBe("新歌词"); + expect(updated.language).toBe("zh"); + expect(updated.isTranslated).toBe(false); + expect(updated.ttml).toBe("original"); + }); + }); + + describe("softDeleteLyric", () => { + test("should soft delete a lyric", async () => { + const song = await repository.create({ name: "Song" }); + const created = await repository.createLyrics(song.id, { + language: "zh", + isTranslated: false, + plainText: "将被删除", + }); + + await repository.softDeleteLyric(created.id); + + const lyric = await repository.getLyricById(created.id); + expect(lyric).toBeNull(); + }); + }); }); diff --git a/packages/core/tests/unit/SongSearchService.test.ts b/packages/core/tests/unit/SongSearchService.test.ts index 3da6704..29fb055 100644 --- a/packages/core/tests/unit/SongSearchService.test.ts +++ b/packages/core/tests/unit/SongSearchService.test.ts @@ -44,7 +44,6 @@ const mockSongDetails: SongDetailsResponseDto = { description: "A test song", coverUrl: "https://example.com/cover.jpg", publishedAt: new Date("2024-01-01").toISOString(), - deletedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), originalSongId: null, @@ -96,11 +95,23 @@ const mockSongDetails: SongDetailsResponseDto = { language: "zh", plainText: "歌词内容", isTranslated: false, + ttml: "", + lrc: "", + id: 1, + createdAt: "", + updatedAt: "", + songId: 1, }, { language: "en", plainText: "Lyrics content", isTranslated: true, + ttml: "", + lrc: "", + id: 1, + createdAt: "", + updatedAt: "", + songId: 1, }, ], }; diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index f2f142b..370a328 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -12,7 +12,6 @@ const mockSongDetails: SongDetailsResponseDto = { description: "A test song", coverUrl: "https://example.com/cover.jpg", publishedAt: new Date("2024-01-01").toISOString(), - deletedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), originalSongId: null, @@ -45,6 +44,29 @@ describe("SongService", () => { create: async () => mockSongDetails, update: async () => mockSongDetails, softDelete: async () => {}, + createLyrics: async () => ({ + id: 1, + language: "zh", + isTranslated: false, + plainText: "Test lyrics", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + getLyricsBySongId: async () => [], + getLyricById: async () => null, + updateLyric: async () => ({ + id: 1, + language: "zh", + isTranslated: false, + plainText: "Updated lyrics", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + softDeleteLyric: async () => {}, }); const mockSearchService = { @@ -181,4 +203,168 @@ describe("SongService", () => { expect(mockRepository.softDelete).toHaveBeenCalledWith(1); }); }); + + describe("listLyrics", () => { + test("returns lyrics when song exists", async () => { + mockRepository.getLyricsBySongId.mockResolvedValueOnce([ + { + id: 1, + language: "zh", + isTranslated: false, + plainText: "歌词", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]); + + const result = await songService.listLyrics(1); + + expect(result).toHaveLength(1); + expect(result[0].language).toBe("zh"); + expect(mockRepository.getLyricsBySongId).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.listLyrics(999)).rejects.toThrow(AppError); + expect(songService.listLyrics(999)).rejects.toThrow("error.song.notfound"); + }); + }); + + describe("getLyric", () => { + test("returns lyric when song and lyric exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce({ + id: 1, + language: "zh", + isTranslated: false, + plainText: "歌词", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + const result = await songService.getLyric(1, 1); + + expect(result.id).toBe(1); + expect(result.plainText).toBe("歌词"); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.getLyric(999, 1)).rejects.toThrow(AppError); + expect(songService.getLyric(999, 1)).rejects.toThrow("error.song.notfound"); + }); + + test("throws NOT_FOUND error when lyric does not exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce(null); + + expect(songService.getLyric(1, 999)).rejects.toThrow(AppError); + expect(songService.getLyric(1, 999)).rejects.toThrow("error.lyric.notfound"); + }); + }); + + describe("createLyric", () => { + const createInput = { + language: "ja" as const, + isTranslated: true, + plainText: "日本語の歌詞", + }; + + test("creates lyric when song exists", async () => { + const result = await songService.createLyric(1, createInput); + + expect(result).toMatchObject({ + language: "zh", + plainText: "Test lyrics", + }); + expect(mockRepository.createLyrics).toHaveBeenCalledWith(1, createInput); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.createLyric(999, createInput)).rejects.toThrow(AppError); + expect(songService.createLyric(999, createInput)).rejects.toThrow( + "error.song.notfound" + ); + }); + }); + + describe("updateLyric", () => { + const updateInput = { plainText: "Updated lyrics" }; + + test("updates lyric when song and lyric exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce({ + id: 1, + language: "zh", + isTranslated: false, + plainText: "原歌词", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + const result = await songService.updateLyric(1, 1, updateInput); + + expect(result.plainText).toBe("Updated lyrics"); + expect(mockRepository.updateLyric).toHaveBeenCalledWith(1, updateInput); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.updateLyric(999, 1, updateInput)).rejects.toThrow(AppError); + expect(songService.updateLyric(999, 1, updateInput)).rejects.toThrow( + "error.song.notfound" + ); + }); + + test("throws NOT_FOUND error when lyric does not exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce(null); + + expect(songService.updateLyric(1, 999, updateInput)).rejects.toThrow(AppError); + expect(songService.updateLyric(1, 999, updateInput)).rejects.toThrow( + "error.lyric.notfound" + ); + }); + }); + + describe("deleteLyric", () => { + test("soft deletes lyric when song and lyric exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce({ + id: 1, + language: "zh", + isTranslated: false, + plainText: "歌词", + ttml: null, + lrc: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + await songService.deleteLyric(1, 1); + + expect(mockRepository.softDeleteLyric).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when song does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(songService.deleteLyric(999, 1)).rejects.toThrow(AppError); + expect(songService.deleteLyric(999, 1)).rejects.toThrow("error.song.notfound"); + }); + + test("throws NOT_FOUND error when lyric does not exist", async () => { + mockRepository.getLyricById.mockResolvedValueOnce(null); + + expect(songService.deleteLyric(1, 999)).rejects.toThrow(AppError); + expect(songService.deleteLyric(1, 999)).rejects.toThrow("error.lyric.notfound"); + }); + }); }); From 0a714c256f95169e0364070a6161b38678a0d746 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sat, 18 Apr 2026 21:07:35 +0800 Subject: [PATCH 15/37] feat(engine): add CRUD endpoints for SVS engine (#49) Add full CRUD endpoints for managing SVS engines in the catalog: - GET /v2/engine/:id/details - retrieve engine details - POST /v2/engine - create new engine (auth required) - PATCH /v2/engine/:id - update engine (auth required) - DELETE /v2/engine/:id - soft delete engine (auth required) Also includes minor fixes to song service for missing search sync calls on lyrics operations. --- .../src/handlers/catalog/engine/create.ts | 33 +++ .../src/handlers/catalog/engine/delete.ts | 34 +++ .../src/handlers/catalog/engine/details.ts | 29 +++ .../src/handlers/catalog/engine/index.ts | 11 + .../src/handlers/catalog/engine/update.ts | 39 +++ apps/backend/src/handlers/index.ts | 1 + apps/backend/src/index.ts | 3 +- apps/backend/tests/engine.test.ts | 237 ++++++++++++++++++ .../src/modules/catalog/engine/container.ts | 6 + .../core/src/modules/catalog/engine/dto.ts | 25 ++ .../core/src/modules/catalog/engine/index.ts | 5 + .../catalog/engine/repository.interface.ts | 23 ++ .../src/modules/catalog/engine/repository.ts | 64 +++++ .../src/modules/catalog/engine/service.ts | 50 ++++ packages/core/src/modules/catalog/index.ts | 1 + .../core/src/modules/catalog/song/service.ts | 21 +- .../integration/SongSearchService.test.ts | 6 - 17 files changed, 579 insertions(+), 9 deletions(-) create mode 100644 apps/backend/src/handlers/catalog/engine/create.ts create mode 100644 apps/backend/src/handlers/catalog/engine/delete.ts create mode 100644 apps/backend/src/handlers/catalog/engine/details.ts create mode 100644 apps/backend/src/handlers/catalog/engine/index.ts create mode 100644 apps/backend/src/handlers/catalog/engine/update.ts create mode 100644 apps/backend/tests/engine.test.ts create mode 100644 packages/core/src/modules/catalog/engine/container.ts create mode 100644 packages/core/src/modules/catalog/engine/dto.ts create mode 100644 packages/core/src/modules/catalog/engine/index.ts create mode 100644 packages/core/src/modules/catalog/engine/repository.interface.ts create mode 100644 packages/core/src/modules/catalog/engine/repository.ts create mode 100644 packages/core/src/modules/catalog/engine/service.ts diff --git a/apps/backend/src/handlers/catalog/engine/create.ts b/apps/backend/src/handlers/catalog/engine/create.ts new file mode 100644 index 0000000..95243e6 --- /dev/null +++ b/apps/backend/src/handlers/catalog/engine/create.ts @@ -0,0 +1,33 @@ +import { Elysia } from "elysia"; +import { + CreateEngineRequestSchema, + ErrorResponseSchema, + EngineResponseSchema, + engineService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const engineCreateHandler = new Elysia({ name: "engineCreateHandler" }) + .use(authMiddleware) + .post( + "/engine", + async ({ body, status }) => { + const engine = await traceTask("engineService.create", async () => { + return await engineService.create(body); + }); + return status(201, engine); + }, + { + body: CreateEngineRequestSchema, + detail: { + summary: "Create Engine", + description: + "Create a new SVS engine entry in the catalog. Requires authentication.", + }, + response: { + 201: EngineResponseSchema, + 401: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/engine/delete.ts b/apps/backend/src/handlers/catalog/engine/delete.ts new file mode 100644 index 0000000..b483c5a --- /dev/null +++ b/apps/backend/src/handlers/catalog/engine/delete.ts @@ -0,0 +1,34 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { ErrorResponseSchema, engineService } from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const engineDeleteHandler = new Elysia({ name: "engineDeleteHandler" }) + .use(authMiddleware) + .delete( + "/engine/:id", + async ({ params, set }) => { + await traceTask("engineService.delete", async () => { + return await engineService.delete(params.id); + }); + set.status = 204; + return null; + }, + { + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Delete Engine", + description: + "Soft delete an SVS engine from the catalog. Requires authentication. The engine record is marked as deleted but retained in the database for data integrity.", + }, + response: { + 204: z.null(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/engine/details.ts b/apps/backend/src/handlers/catalog/engine/details.ts new file mode 100644 index 0000000..3233e5c --- /dev/null +++ b/apps/backend/src/handlers/catalog/engine/details.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, EngineDetailsResponseSchema, engineService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const engineDetailsHandler = new Elysia().get( + "/engine/:id/details", + async ({ params, status }) => { + const engine = await traceTask("engineService.getDetails", async () => { + return await engineService.getDetails(params.id); + }); + return status(200, engine); + }, + { + detail: { + summary: "Engine Details", + description: + "Retrieve detailed information about a specific SVS engine by its ID. Includes name, description, and metadata.", + }, + response: { + 200: EngineDetailsResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/engine/index.ts b/apps/backend/src/handlers/catalog/engine/index.ts new file mode 100644 index 0000000..fee3441 --- /dev/null +++ b/apps/backend/src/handlers/catalog/engine/index.ts @@ -0,0 +1,11 @@ +import { Elysia } from "elysia"; +import { engineCreateHandler } from "./create"; +import { engineUpdateHandler } from "./update"; +import { engineDeleteHandler } from "./delete"; +import { engineDetailsHandler } from "./details"; + +export const engineHandler = new Elysia({ name: "engineHandler" }) + .use(engineDetailsHandler) + .use(engineCreateHandler) + .use(engineUpdateHandler) + .use(engineDeleteHandler); diff --git a/apps/backend/src/handlers/catalog/engine/update.ts b/apps/backend/src/handlers/catalog/engine/update.ts new file mode 100644 index 0000000..90c1193 --- /dev/null +++ b/apps/backend/src/handlers/catalog/engine/update.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { + UpdateEngineRequestSchema, + ErrorResponseSchema, + engineService, + EngineResponseSchema, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const engineUpdateHandler = new Elysia({ name: "engineUpdateHandler" }) + .use(authMiddleware) + .patch( + "/engine/:id", + async ({ params, body, status }) => { + const engine = await traceTask("engineService.update", async () => { + return await engineService.update(params.id, body); + }); + return status(200, engine); + }, + { + body: UpdateEngineRequestSchema, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Update Engine", + description: + "Update metadata of an existing SVS engine identified by its ID. Requires authentication.", + }, + response: { + 200: EngineResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/index.ts b/apps/backend/src/handlers/index.ts index 190d46f..0b8a509 100644 --- a/apps/backend/src/handlers/index.ts +++ b/apps/backend/src/handlers/index.ts @@ -1,3 +1,4 @@ export * from "./auth"; export * from "./catalog/song"; +export * from "./catalog/engine"; export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index ecdb0e6..c4acb23 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,7 +2,7 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler, songHandler } from "@handlers/index"; +import { authHandler, songHandler, engineHandler } from "@handlers/index"; import { errorHandler } from "./errorHandler"; import { openapi } from "@elysiajs/openapi"; import { requestLoggerMiddleware } from "@/middlewares"; @@ -32,6 +32,7 @@ export const app = new Elysia({ .use(openapi()) .use(authHandler) .use(songHandler) + .use(engineHandler) .use(devHandler) .listen(16412); diff --git a/apps/backend/tests/engine.test.ts b/apps/backend/tests/engine.test.ts new file mode 100644 index 0000000..4c42cc5 --- /dev/null +++ b/apps/backend/tests/engine.test.ts @@ -0,0 +1,237 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@cvsa/db"; + +const api = treaty(app); + +describe("Engine E2E Tests", () => { + beforeAll(async () => { + await prisma.$connect(); + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.svsEngine.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.$disconnect(); + }); + + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: `${Math.random()}`, + password: "password123", + }); + return signup.data?.data.token; + } + + describe("GET /v2/engine/:id/details - Get Engine Details", () => { + test("should retrieve an engine", async () => { + const engine = await prisma.svsEngine.create({ + data: { + name: "Test Engine", + description: "Test Description", + }, + }); + + const { data, status } = await api.v2.engine({ id: engine.id }).details.get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Test Engine", + description: "Test Description", + }); + }); + + test("should return 404 for nonexistent engine", async () => { + const { error, status } = await api.v2.engine({ id: 99999 }).details.get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("POST /v2/engine - Create Engine", () => { + test("should create an engine with authentication", async () => { + const token = await getAuthToken(); + + const payload = { + name: "VOCALOID", + description: "A singing voice synthesis software", + }; + + const { data, status } = await api.v2.engine.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "VOCALOID", + description: "A singing voice synthesis software", + }); + }); + + test("should return 401 without authentication", async () => { + const payload = { + name: "Test Engine", + }; + + const { error, status } = await api.v2.engine.post(payload); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should create an engine with localized descriptions", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Synthesizer V", + description: "A singing voice synthesis software", + localizedDescriptions: { + zh: "歌声合成软件", + ja: "歌声合成ソフトウェア", + }, + }; + + const { data, status } = await api.v2.engine.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Synthesizer V", + localizedDescriptions: { + zh: "歌声合成软件", + ja: "歌声合成ソフトウェア", + }, + }); + }); + }); + + describe("PATCH /v2/engine/:id - Update Engine", () => { + test("should update an engine with authentication", async () => { + const token = await getAuthToken(); + + const engine = await prisma.svsEngine.create({ + data: { + name: "Original Name", + description: "Original Description", + }, + }); + + const { data, status } = await api.v2.engine({ id: engine.id }).patch( + { name: "Updated Name", description: "Updated Description" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.name).toBe("Updated Name"); + expect(data?.description).toBe("Updated Description"); + }); + + test("should return 401 without authentication", async () => { + const engine = await prisma.svsEngine.create({ + data: { + name: "Test Engine", + }, + }); + + const { status } = await api.v2.engine({ id: engine.id }).patch({ + name: "Updated Name", + }); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent engine", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2.engine({ id: 999999 }).patch( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("DELETE /v2/engine/:id - Delete Engine", () => { + test("should soft delete an engine with authentication", async () => { + const token = await getAuthToken(); + + const engine = await prisma.svsEngine.create({ + data: { + name: "Engine to Delete", + }, + }); + + const { status } = await api.v2.engine({ id: engine.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedEngine = await prisma.svsEngine.findUnique({ + where: { id: engine.id }, + }); + expect(deletedEngine?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const engine = await prisma.svsEngine.create({ + data: { + name: "Test Engine", + }, + }); + + const { status } = await api.v2.engine({ id: engine.id }).delete({}, {}); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent engine", async () => { + const token = await getAuthToken(); + + const { status } = await api.v2.engine({ id: 999999 }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + }); + }); +}); diff --git a/packages/core/src/modules/catalog/engine/container.ts b/packages/core/src/modules/catalog/engine/container.ts new file mode 100644 index 0000000..d66d88d --- /dev/null +++ b/packages/core/src/modules/catalog/engine/container.ts @@ -0,0 +1,6 @@ +import { prisma } from "@cvsa/db"; +import { EngineRepository } from "./repository"; +import { EngineService } from "./service"; + +export const engineRepository = new EngineRepository(prisma); +export const engineService = new EngineService(engineRepository); diff --git a/packages/core/src/modules/catalog/engine/dto.ts b/packages/core/src/modules/catalog/engine/dto.ts new file mode 100644 index 0000000..e0e789e --- /dev/null +++ b/packages/core/src/modules/catalog/engine/dto.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { SvsEngineSchema, type Serialized } from "@cvsa/db"; + +export type EngineId = number; + +export const CreateEngineRequestSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const UpdateEngineRequestSchema = z.object({ + name: z.string().min(1).max(255).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const EngineResponseSchema = SvsEngineSchema.omit({ deletedAt: true }); + +export const EngineDetailsResponseSchema = EngineResponseSchema; + +export type EngineResponseDto = Serialized>; +export type EngineDetailsResponseDto = Serialized>; +export type CreateEngineRequestDto = Serialized>; +export type UpdateEngineRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/engine/index.ts b/packages/core/src/modules/catalog/engine/index.ts new file mode 100644 index 0000000..132cf0c --- /dev/null +++ b/packages/core/src/modules/catalog/engine/index.ts @@ -0,0 +1,5 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./service"; +export * from "./repository.interface"; +export * from "./container"; diff --git a/packages/core/src/modules/catalog/engine/repository.interface.ts b/packages/core/src/modules/catalog/engine/repository.interface.ts new file mode 100644 index 0000000..a106f1c --- /dev/null +++ b/packages/core/src/modules/catalog/engine/repository.interface.ts @@ -0,0 +1,23 @@ +import type { TxClient } from "@cvsa/db"; +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; +import type { + CreateEngineRequestDto, + EngineId, + UpdateEngineRequestDto, + EngineResponseDto, + EngineDetailsResponseDto, +} from "./dto"; + +export abstract class IEngineRepository + implements IRepositoryWithGetDetails +{ + abstract getById(id: EngineId, tx?: TxClient): Promise; + abstract getDetailsById(id: EngineId, tx?: TxClient): Promise; + abstract create(input: CreateEngineRequestDto, tx?: TxClient): Promise; + abstract update( + id: EngineId, + input: UpdateEngineRequestDto, + tx?: TxClient + ): Promise; + abstract softDelete(id: EngineId, tx?: TxClient): Promise; +} diff --git a/packages/core/src/modules/catalog/engine/repository.ts b/packages/core/src/modules/catalog/engine/repository.ts new file mode 100644 index 0000000..23670e2 --- /dev/null +++ b/packages/core/src/modules/catalog/engine/repository.ts @@ -0,0 +1,64 @@ +import type { PrismaClient } from "@cvsa/db"; +import type { + CreateEngineRequestDto, + EngineId, + UpdateEngineRequestDto, + EngineDetailsResponseDto, +} from "./dto"; +import type { IEngineRepository } from "./repository.interface"; +import { transformPrismaResult, type TxClient } from "@cvsa/db"; + +export class EngineRepository implements IEngineRepository { + constructor(private readonly prisma: PrismaClient) {} + + async getById(id: EngineId, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.svsEngine.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async getDetailsById(id: EngineId, tx?: TxClient): Promise { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.svsEngine.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async create(input: CreateEngineRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.svsEngine.create({ + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async update(id: EngineId, input: UpdateEngineRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.svsEngine.update({ + where: { id }, + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async softDelete(id: EngineId, tx?: TxClient) { + const client = tx ?? this.prisma; + await client.svsEngine.update({ + where: { id }, + data: { deletedAt: new Date() }, + }); + } +} diff --git a/packages/core/src/modules/catalog/engine/service.ts b/packages/core/src/modules/catalog/engine/service.ts new file mode 100644 index 0000000..6f46ea5 --- /dev/null +++ b/packages/core/src/modules/catalog/engine/service.ts @@ -0,0 +1,50 @@ +import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import type { + EngineDetailsResponseDto, + EngineId, + CreateEngineRequestDto, + UpdateEngineRequestDto, + EngineResponseDto, +} from "./dto"; +import type { IEngineRepository } from "./repository.interface"; +import { traceTask } from "@cvsa/observability"; + +export class EngineService implements IServiceWithGetDetails { + constructor(private readonly repository: IEngineRepository) {} + + async getDetails(id: EngineId) { + return traceTask("db findOne engine", async () => { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.engine.notfound", "NOT_FOUND", 404); + } + return result; + }); + } + + async create(input: CreateEngineRequestDto): Promise { + return traceTask("db create engine", async () => { + return await this.repository.create(input); + }); + } + + async update(id: EngineId, input: UpdateEngineRequestDto): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.engine.notfound", "NOT_FOUND", 404); + } + return traceTask("db update engine", async () => { + return await this.repository.update(id, input); + }); + } + + async delete(id: EngineId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.engine.notfound", "NOT_FOUND", 404); + } + await traceTask("db delete engine", async () => { + return await this.repository.softDelete(id); + }); + } +} diff --git a/packages/core/src/modules/catalog/index.ts b/packages/core/src/modules/catalog/index.ts index 153b6ce..69b91cc 100644 --- a/packages/core/src/modules/catalog/index.ts +++ b/packages/core/src/modules/catalog/index.ts @@ -1 +1,2 @@ export * from "./song"; +export * from "./engine"; diff --git a/packages/core/src/modules/catalog/song/service.ts b/packages/core/src/modules/catalog/song/service.ts index ec58fa1..8e9a6ba 100644 --- a/packages/core/src/modules/catalog/song/service.ts +++ b/packages/core/src/modules/catalog/song/service.ts @@ -107,9 +107,15 @@ export class SongService implements IServiceWithGetDetails { + const result = await traceTask("db create lyric", async () => { return await this.repository.createLyrics(id, input); }); + await traceTask("sync search index", async () => { + this.search.sync(id).catch((e) => { + appLogger.warn(Bun.inspect(e)); + }); + }); + return result; } async updateLyric( @@ -125,9 +131,15 @@ export class SongService implements IServiceWithGetDetails { + const result = await traceTask("db update lyric", async () => { return await this.repository.updateLyric(lyricId, input); }); + await traceTask("sync search index", async () => { + this.search.sync(id).catch((e) => { + appLogger.warn(Bun.inspect(e)); + }); + }); + return result; } async deleteLyric(id: SongId, lyricId: number): Promise { @@ -142,5 +154,10 @@ export class SongService implements IServiceWithGetDetails { return await this.repository.softDeleteLyric(lyricId); }); + await traceTask("sync search index", async () => { + this.search.sync(id).catch((e) => { + appLogger.warn(Bun.inspect(e)); + }); + }); } } diff --git a/packages/core/tests/integration/SongSearchService.test.ts b/packages/core/tests/integration/SongSearchService.test.ts index 3bfbd96..1d1e721 100644 --- a/packages/core/tests/integration/SongSearchService.test.ts +++ b/packages/core/tests/integration/SongSearchService.test.ts @@ -6,12 +6,6 @@ import type { EmbeddingAppApi } from "@cvsa/embedding"; import { SearchManager } from "../../src/search/manager"; import { env } from "@cvsa/env"; import { MeiliSearch } from "meilisearch"; -// import { OriginalMeiliSearch } from "../utils"; -// import type { MeiliSearch as MeiliType } from "meilisearch"; - -// const MeiliSearch = OriginalMeiliSearch.MeiliSearch; -// console.debug("CCCCCCC", MeiliSearch); -// type MeiliSearch = MeiliType; const MEILI_HOST = env.MEILI_API_URL ?? "http://127.0.0.1:7700"; const MEILI_MASTER_KEY = env.MEILI_MASTER_KEY ?? ""; From 754117c673dac83ac1d00d3c83b9df8d1821d357 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Mon, 20 Apr 2026 18:55:25 +0800 Subject: [PATCH 16/37] feat(artist): add CRUD endpoints for artists (#51) Implement a complete CRUD API for managing artists in the catalog. Changes: - Add GET /v2/artist/:id endpoint for retrieving artist details - Add POST /v2/artist endpoint for creating new artists (auth required) - Add PATCH /v2/artist/:id endpoint for updating artist metadata (auth required) - Add DELETE /v2/artist/:id endpoint for soft-deleting artists (auth required) - Add Zod schemas for request/response validation in packages/core - Add E2E tests covering all endpoints with 15 test cases --- .../src/handlers/catalog/artist/create.ts | 32 ++ .../src/handlers/catalog/artist/delete.ts | 34 ++ .../src/handlers/catalog/artist/get.ts | 29 ++ .../src/handlers/catalog/artist/index.ts | 11 + .../src/handlers/catalog/artist/update.ts | 39 ++ apps/backend/src/handlers/index.ts | 1 + apps/backend/src/index.ts | 3 +- apps/backend/tests/artist.test.ts | 338 ++++++++++++++++++ .../src/modules/catalog/artist/container.ts | 6 + .../core/src/modules/catalog/artist/dto.ts | 31 ++ .../core/src/modules/catalog/artist/index.ts | 5 + .../catalog/artist/repository.interface.ts | 23 ++ .../src/modules/catalog/artist/repository.ts | 64 ++++ .../src/modules/catalog/artist/service.ts | 50 +++ packages/core/src/modules/catalog/index.ts | 1 + 15 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 apps/backend/src/handlers/catalog/artist/create.ts create mode 100644 apps/backend/src/handlers/catalog/artist/delete.ts create mode 100644 apps/backend/src/handlers/catalog/artist/get.ts create mode 100644 apps/backend/src/handlers/catalog/artist/index.ts create mode 100644 apps/backend/src/handlers/catalog/artist/update.ts create mode 100644 apps/backend/tests/artist.test.ts create mode 100644 packages/core/src/modules/catalog/artist/container.ts create mode 100644 packages/core/src/modules/catalog/artist/dto.ts create mode 100644 packages/core/src/modules/catalog/artist/index.ts create mode 100644 packages/core/src/modules/catalog/artist/repository.interface.ts create mode 100644 packages/core/src/modules/catalog/artist/repository.ts create mode 100644 packages/core/src/modules/catalog/artist/service.ts diff --git a/apps/backend/src/handlers/catalog/artist/create.ts b/apps/backend/src/handlers/catalog/artist/create.ts new file mode 100644 index 0000000..d4de3d0 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/create.ts @@ -0,0 +1,32 @@ +import { Elysia } from "elysia"; +import { + CreateArtistRequestSchema, + ErrorResponseSchema, + ArtistResponseSchema, + artistService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistCreateHandler = new Elysia({ name: "artistCreateHandler" }) + .use(authMiddleware) + .post( + "/artist", + async ({ body, status }) => { + const artist = await traceTask("artistService.create", async () => { + return await artistService.create(body); + }); + return status(201, artist); + }, + { + body: CreateArtistRequestSchema, + detail: { + summary: "Create Artist", + description: "Create a new artist entry in the catalog. Requires authentication.", + }, + response: { + 201: ArtistResponseSchema, + 401: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/artist/delete.ts b/apps/backend/src/handlers/catalog/artist/delete.ts new file mode 100644 index 0000000..d6abec0 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/delete.ts @@ -0,0 +1,34 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { ErrorResponseSchema, artistService } from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistDeleteHandler = new Elysia({ name: "artistDeleteHandler" }) + .use(authMiddleware) + .delete( + "/artist/:id", + async ({ params, set }) => { + await traceTask("artistService.delete", async () => { + return await artistService.delete(params.id); + }); + set.status = 204; + return null; + }, + { + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Delete Artist", + description: + "Soft delete an artist from the catalog. Requires authentication. The artist record is marked as deleted but retained in the database for data integrity.", + }, + response: { + 204: z.null(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/artist/get.ts b/apps/backend/src/handlers/catalog/artist/get.ts new file mode 100644 index 0000000..9e8096f --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/get.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, ArtistDetailsResponseSchema, artistService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const artistDetailsHandler = new Elysia().get( + "/artist/:id", + async ({ params, status }) => { + const artist = await traceTask("artistService.getDetails", async () => { + return await artistService.getDetails(params.id); + }); + return status(200, artist); + }, + { + detail: { + summary: "Artist Details", + description: + "Retrieve detailed information about a specific artist by its ID. Includes name, description, aliases, and metadata.", + }, + response: { + 200: ArtistDetailsResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/artist/index.ts b/apps/backend/src/handlers/catalog/artist/index.ts new file mode 100644 index 0000000..498c5f5 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/index.ts @@ -0,0 +1,11 @@ +import { Elysia } from "elysia"; +import { artistCreateHandler } from "./create"; +import { artistUpdateHandler } from "./update"; +import { artistDeleteHandler } from "./delete"; +import { artistDetailsHandler } from "./get"; + +export const artistHandler = new Elysia({ name: "artistHandler" }) + .use(artistDetailsHandler) + .use(artistCreateHandler) + .use(artistUpdateHandler) + .use(artistDeleteHandler); diff --git a/apps/backend/src/handlers/catalog/artist/update.ts b/apps/backend/src/handlers/catalog/artist/update.ts new file mode 100644 index 0000000..6162564 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/update.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { + UpdateArtistRequestSchema, + ErrorResponseSchema, + artistService, + ArtistResponseSchema, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistUpdateHandler = new Elysia({ name: "artistUpdateHandler" }) + .use(authMiddleware) + .patch( + "/artist/:id", + async ({ params, body, status }) => { + const artist = await traceTask("artistService.update", async () => { + return await artistService.update(params.id, body); + }); + return status(200, artist); + }, + { + body: UpdateArtistRequestSchema, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Update Artist", + description: + "Update metadata of an existing artist identified by its ID. Requires authentication.", + }, + response: { + 200: ArtistResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/index.ts b/apps/backend/src/handlers/index.ts index 0b8a509..768f891 100644 --- a/apps/backend/src/handlers/index.ts +++ b/apps/backend/src/handlers/index.ts @@ -1,4 +1,5 @@ export * from "./auth"; export * from "./catalog/song"; export * from "./catalog/engine"; +export * from "./catalog/artist"; export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c4acb23..216dfd7 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,7 +2,7 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler, songHandler, engineHandler } from "@handlers/index"; +import { authHandler, songHandler, engineHandler, artistHandler } from "@handlers/index"; import { errorHandler } from "./errorHandler"; import { openapi } from "@elysiajs/openapi"; import { requestLoggerMiddleware } from "@/middlewares"; @@ -33,6 +33,7 @@ export const app = new Elysia({ .use(authHandler) .use(songHandler) .use(engineHandler) + .use(artistHandler) .use(devHandler) .listen(16412); diff --git a/apps/backend/tests/artist.test.ts b/apps/backend/tests/artist.test.ts new file mode 100644 index 0000000..93f7f6f --- /dev/null +++ b/apps/backend/tests/artist.test.ts @@ -0,0 +1,338 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@cvsa/db"; + +const api = treaty(app); + +describe("Artist E2E Tests", () => { + beforeAll(async () => { + await prisma.$connect(); + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.artist.deleteMany(); + }); + + afterAll(async () => { + await prisma.session.deleteMany(); + await prisma.user.deleteMany(); + await prisma.$disconnect(); + }); + + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: `${Math.random()}`, + password: "password123", + }); + return signup.data?.data.token; + } + + describe("GET /v2/artist/:id - Get Artist Details", () => { + test("should retrieve an artist", async () => { + const artist = await prisma.artist.create({ + data: { + name: "Test Artist", + description: "Test Description", + language: "zh", + aliases: ["Alias1", "Alias2"], + }, + }); + + const { data, status } = await api.v2.artist({ id: artist.id }).get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Test Artist", + description: "Test Description", + language: "zh", + aliases: ["Alias1", "Alias2"], + }); + }); + + test("should return 404 for nonexistent artist", async () => { + const { error, status } = await api.v2.artist({ id: 99999 }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should not retrieve soft-deleted artist", async () => { + const artist = await prisma.artist.create({ + data: { + name: "Deleted Artist", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2.artist({ id: artist.id }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("POST /v2/artist - Create Artist", () => { + test("should create an artist with authentication", async () => { + const token = await getAuthToken(); + + const payload = { + name: "New Artist", + description: "A talented producer", + language: "zh", + aliases: ["Producer X", "PX"], + }; + + const { data, status } = await api.v2.artist.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "New Artist", + description: "A talented producer", + language: "zh", + aliases: ["Producer X", "PX"], + }); + }); + + test("should return 401 without authentication", async () => { + const payload = { + name: "Test Artist", + }; + + const { error, status } = await api.v2.artist.post(payload); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should create an artist with localized names and descriptions", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Artist Name", + localizedNames: { + zh: "艺术家名称", + ja: "アーティスト名", + }, + description: "Description in English", + localizedDescriptions: { + zh: "中文描述", + ja: "日本語の説明", + }, + language: "zh", + }; + + const { data, status } = await api.v2.artist.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Artist Name", + localizedNames: { + zh: "艺术家名称", + ja: "アーティスト名", + }, + localizedDescriptions: { + zh: "中文描述", + ja: "日本語の説明", + }, + }); + }); + + test("should create an artist with minimal fields", async () => { + const token = await getAuthToken(); + + const { data, status } = await api.v2.artist.post( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + }); + }); + }); + + describe("PATCH /v2/artist/:id - Update Artist", () => { + test("should update an artist with authentication", async () => { + const token = await getAuthToken(); + + const artist = await prisma.artist.create({ + data: { + name: "Original Name", + description: "Original Description", + language: "zh", + }, + }); + + const { data, status } = await api.v2.artist({ id: artist.id }).patch( + { name: "Updated Name", description: "Updated Description", language: "en" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.name).toBe("Updated Name"); + expect(data?.description).toBe("Updated Description"); + expect(data?.language).toBe("en"); + }); + + test("should return 401 without authentication", async () => { + const artist = await prisma.artist.create({ + data: { + name: "Test Artist", + }, + }); + + const { status } = await api.v2.artist({ id: artist.id }).patch({ + name: "Updated Name", + }); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent artist", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2.artist({ id: 999999 }).patch( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should update aliases array", async () => { + const token = await getAuthToken(); + + const artist = await prisma.artist.create({ + data: { + name: "Artist", + aliases: ["Old Alias"], + }, + }); + + const { data, status } = await api.v2.artist({ id: artist.id }).patch( + { aliases: ["New Alias 1", "New Alias 2"] }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.aliases).toEqual(["New Alias 1", "New Alias 2"]); + }); + }); + + describe("DELETE /v2/artist/:id - Delete Artist", () => { + test("should soft delete an artist with authentication", async () => { + const token = await getAuthToken(); + + const artist = await prisma.artist.create({ + data: { + name: "Artist to Delete", + }, + }); + + const { status } = await api.v2.artist({ id: artist.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedArtist = await prisma.artist.findUnique({ + where: { id: artist.id }, + }); + expect(deletedArtist?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const artist = await prisma.artist.create({ + data: { + name: "Test Artist", + }, + }); + + const { status } = await api.v2.artist({ id: artist.id }).delete({}, {}); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent artist", async () => { + const token = await getAuthToken(); + + const { status } = await api.v2.artist({ id: 999999 }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + }); + + test("should return 404 when trying to delete already deleted artist", async () => { + const token = await getAuthToken(); + + const artist = await prisma.artist.create({ + data: { + name: "Already Deleted", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2.artist({ id: artist.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); +}); diff --git a/packages/core/src/modules/catalog/artist/container.ts b/packages/core/src/modules/catalog/artist/container.ts new file mode 100644 index 0000000..f8c2c71 --- /dev/null +++ b/packages/core/src/modules/catalog/artist/container.ts @@ -0,0 +1,6 @@ +import { prisma } from "@cvsa/db"; +import { ArtistRepository } from "./repository"; +import { ArtistService } from "./service"; + +export const artistRepository = new ArtistRepository(prisma); +export const artistService = new ArtistService(artistRepository); diff --git a/packages/core/src/modules/catalog/artist/dto.ts b/packages/core/src/modules/catalog/artist/dto.ts new file mode 100644 index 0000000..2ac8d0d --- /dev/null +++ b/packages/core/src/modules/catalog/artist/dto.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { ArtistSchema, type Serialized } from "@cvsa/db"; + +export type ArtistId = number; + +export const CreateArtistRequestSchema = z.object({ + name: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), + language: z.string().optional(), + aliases: z.array(z.string()).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const UpdateArtistRequestSchema = z.object({ + name: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), + language: z.string().optional(), + aliases: z.array(z.string()).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const ArtistResponseSchema = ArtistSchema.omit({ deletedAt: true }); + +export const ArtistDetailsResponseSchema = ArtistResponseSchema; + +export type ArtistResponseDto = Serialized>; +export type ArtistDetailsResponseDto = Serialized>; +export type CreateArtistRequestDto = Serialized>; +export type UpdateArtistRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/artist/index.ts b/packages/core/src/modules/catalog/artist/index.ts new file mode 100644 index 0000000..132cf0c --- /dev/null +++ b/packages/core/src/modules/catalog/artist/index.ts @@ -0,0 +1,5 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./service"; +export * from "./repository.interface"; +export * from "./container"; diff --git a/packages/core/src/modules/catalog/artist/repository.interface.ts b/packages/core/src/modules/catalog/artist/repository.interface.ts new file mode 100644 index 0000000..4f286aa --- /dev/null +++ b/packages/core/src/modules/catalog/artist/repository.interface.ts @@ -0,0 +1,23 @@ +import type { TxClient } from "@cvsa/db"; +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; +import type { + CreateArtistRequestDto, + ArtistId, + UpdateArtistRequestDto, + ArtistResponseDto, + ArtistDetailsResponseDto, +} from "./dto"; + +export abstract class IArtistRepository + implements IRepositoryWithGetDetails +{ + abstract getById(id: ArtistId, tx?: TxClient): Promise; + abstract getDetailsById(id: ArtistId, tx?: TxClient): Promise; + abstract create(input: CreateArtistRequestDto, tx?: TxClient): Promise; + abstract update( + id: ArtistId, + input: UpdateArtistRequestDto, + tx?: TxClient + ): Promise; + abstract softDelete(id: ArtistId, tx?: TxClient): Promise; +} diff --git a/packages/core/src/modules/catalog/artist/repository.ts b/packages/core/src/modules/catalog/artist/repository.ts new file mode 100644 index 0000000..0897f52 --- /dev/null +++ b/packages/core/src/modules/catalog/artist/repository.ts @@ -0,0 +1,64 @@ +import type { PrismaClient } from "@cvsa/db"; +import type { + CreateArtistRequestDto, + ArtistId, + UpdateArtistRequestDto, + ArtistDetailsResponseDto, +} from "./dto"; +import type { IArtistRepository } from "./repository.interface"; +import { transformPrismaResult, type TxClient } from "@cvsa/db"; + +export class ArtistRepository implements IArtistRepository { + constructor(private readonly prisma: PrismaClient) {} + + async getById(id: ArtistId, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.artist.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async getDetailsById(id: ArtistId, tx?: TxClient): Promise { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.artist.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async create(input: CreateArtistRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.artist.create({ + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async update(id: ArtistId, input: UpdateArtistRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + + return transformPrismaResult( + await client.artist.update({ + where: { id }, + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async softDelete(id: ArtistId, tx?: TxClient) { + const client = tx ?? this.prisma; + await client.artist.update({ + where: { id }, + data: { deletedAt: new Date() }, + }); + } +} diff --git a/packages/core/src/modules/catalog/artist/service.ts b/packages/core/src/modules/catalog/artist/service.ts new file mode 100644 index 0000000..d688934 --- /dev/null +++ b/packages/core/src/modules/catalog/artist/service.ts @@ -0,0 +1,50 @@ +import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import type { + ArtistDetailsResponseDto, + ArtistId, + CreateArtistRequestDto, + UpdateArtistRequestDto, + ArtistResponseDto, +} from "./dto"; +import type { IArtistRepository } from "./repository.interface"; +import { traceTask } from "@cvsa/observability"; + +export class ArtistService implements IServiceWithGetDetails { + constructor(private readonly repository: IArtistRepository) {} + + async getDetails(id: ArtistId) { + return traceTask("db findOne artist", async () => { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.artist.notfound", "NOT_FOUND", 404); + } + return result; + }); + } + + async create(input: CreateArtistRequestDto): Promise { + return traceTask("db create artist", async () => { + return await this.repository.create(input); + }); + } + + async update(id: ArtistId, input: UpdateArtistRequestDto): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.artist.notfound", "NOT_FOUND", 404); + } + return traceTask("db update artist", async () => { + return await this.repository.update(id, input); + }); + } + + async delete(id: ArtistId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.artist.notfound", "NOT_FOUND", 404); + } + await traceTask("db delete artist", async () => { + return await this.repository.softDelete(id); + }); + } +} diff --git a/packages/core/src/modules/catalog/index.ts b/packages/core/src/modules/catalog/index.ts index 69b91cc..6b142ba 100644 --- a/packages/core/src/modules/catalog/index.ts +++ b/packages/core/src/modules/catalog/index.ts @@ -1,2 +1,3 @@ export * from "./song"; export * from "./engine"; +export * from "./artist"; From fff3b119c80bb986156189edc612412f0468397f Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 26 Apr 2026 17:21:17 +0800 Subject: [PATCH 17/37] feat(artist): search service for artists (#53) Replace fire-and-forget search index synchronization with a transactional outbox pattern using BullMQ to guarantee eventual consistency between DB and search index. Add artist search service with hybrid embedding search. Introduce BaseRepository to flatten repository code, add comprehensive unit/integration tests for outbox, artist, and engine modules, and add missing i18n error keys. --- .../src/handlers/catalog/artist/index.ts | 4 +- .../src/handlers/catalog/artist/search.ts | 25 ++ apps/backend/src/index.ts | 24 ++ apps/backend/tests/artist.test.ts | 15 +- apps/backend/tests/auth.test.ts | 53 +-- apps/backend/tests/engine.test.ts | 15 +- apps/backend/tests/song.test.ts | 17 +- apps/backend/tests/songLyrics.test.ts | 18 +- bun.lock | 69 +++- docker-compose.yml | 10 + locale/modules/backend/messages/en.json | 4 +- locale/modules/backend/messages/zh.json | 4 +- package.json | 6 +- packages/core/bunfig.toml | 2 + packages/core/package.json | 4 + packages/core/scripts/cleanUpSearchIndex.ts | 13 + packages/core/src/index.ts | 1 + .../src/modules/catalog/artist/container.ts | 13 +- .../src/modules/catalog/artist/repository.ts | 37 +- .../src/modules/catalog/artist/service.ts | 71 +++- .../src/modules/catalog/engine/repository.ts | 37 +- .../src/modules/catalog/engine/service.ts | 3 +- .../src/modules/catalog/song/container.ts | 9 +- .../src/modules/catalog/song/repository.ts | 58 ++-- .../core/src/modules/catalog/song/service.ts | 159 +++++---- packages/core/src/modules/index.ts | 1 + packages/core/src/modules/outbox/container.ts | 6 + packages/core/src/modules/outbox/dto.ts | 35 ++ packages/core/src/modules/outbox/index.ts | 5 + .../modules/outbox/repository.interface.ts | 15 + .../core/src/modules/outbox/repository.ts | 71 ++++ packages/core/src/modules/outbox/service.ts | 78 +++++ packages/core/src/outbox/index.ts | 2 + packages/core/src/outbox/processor.ts | 48 +++ packages/core/src/outbox/queue.ts | 61 ++++ packages/core/src/search/catalog/artist.ts | 105 ++++++ packages/core/src/search/catalog/index.ts | 1 + packages/core/src/search/catalog/song.ts | 19 +- packages/core/src/search/config.ts | 10 + packages/core/src/search/manager.ts | 45 ++- packages/core/src/utils/BaseRepository.ts | 8 + packages/core/src/utils/index.ts | 1 + .../integration/ArtistRepository.test.ts | 154 +++++++++ .../integration/EngineRepository.test.ts | 142 ++++++++ .../integration/OutboxRepository.test.ts | 322 ++++++++++++++++++ .../tests/integration/SongRepository.test.ts | 24 +- .../integration/SongSearchService.test.ts | 105 +----- .../tests/unit/ArtistSearchService.test.ts | 244 +++++++++++++ .../core/tests/unit/ArtistService.test.ts | 183 ++++++++++ .../core/tests/unit/EngineService.test.ts | 120 +++++++ .../core/tests/unit/OutboxProcessor.test.ts | 116 +++++++ .../core/tests/unit/OutboxService.test.ts | 248 ++++++++++++++ .../core/tests/unit/SearchManager.test.ts | 101 +++++- .../core/tests/unit/SongSearchService.test.ts | 9 +- packages/core/tests/unit/SongService.test.ts | 89 ++--- packages/db/package.json | 1 + packages/db/prisma/models/meta/outbox.prisma | 27 ++ packages/db/src/index.ts | 2 +- packages/embedding/src/embedding.ts | 1 - packages/env/src/index.ts | 2 + turbo.json | 9 + 61 files changed, 2629 insertions(+), 452 deletions(-) create mode 100644 apps/backend/src/handlers/catalog/artist/search.ts create mode 100644 packages/core/scripts/cleanUpSearchIndex.ts create mode 100644 packages/core/src/modules/outbox/container.ts create mode 100644 packages/core/src/modules/outbox/dto.ts create mode 100644 packages/core/src/modules/outbox/index.ts create mode 100644 packages/core/src/modules/outbox/repository.interface.ts create mode 100644 packages/core/src/modules/outbox/repository.ts create mode 100644 packages/core/src/modules/outbox/service.ts create mode 100644 packages/core/src/outbox/index.ts create mode 100644 packages/core/src/outbox/processor.ts create mode 100644 packages/core/src/outbox/queue.ts create mode 100644 packages/core/src/search/catalog/artist.ts create mode 100644 packages/core/src/utils/BaseRepository.ts create mode 100644 packages/core/tests/integration/ArtistRepository.test.ts create mode 100644 packages/core/tests/integration/EngineRepository.test.ts create mode 100644 packages/core/tests/integration/OutboxRepository.test.ts create mode 100644 packages/core/tests/unit/ArtistSearchService.test.ts create mode 100644 packages/core/tests/unit/ArtistService.test.ts create mode 100644 packages/core/tests/unit/EngineService.test.ts create mode 100644 packages/core/tests/unit/OutboxProcessor.test.ts create mode 100644 packages/core/tests/unit/OutboxService.test.ts create mode 100644 packages/db/prisma/models/meta/outbox.prisma diff --git a/apps/backend/src/handlers/catalog/artist/index.ts b/apps/backend/src/handlers/catalog/artist/index.ts index 498c5f5..4adfb99 100644 --- a/apps/backend/src/handlers/catalog/artist/index.ts +++ b/apps/backend/src/handlers/catalog/artist/index.ts @@ -3,9 +3,11 @@ import { artistCreateHandler } from "./create"; import { artistUpdateHandler } from "./update"; import { artistDeleteHandler } from "./delete"; import { artistDetailsHandler } from "./get"; +import { artistSearchHandler } from "./search"; export const artistHandler = new Elysia({ name: "artistHandler" }) .use(artistDetailsHandler) .use(artistCreateHandler) .use(artistUpdateHandler) - .use(artistDeleteHandler); + .use(artistDeleteHandler) + .use(artistSearchHandler); diff --git a/apps/backend/src/handlers/catalog/artist/search.ts b/apps/backend/src/handlers/catalog/artist/search.ts new file mode 100644 index 0000000..fd05288 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artist/search.ts @@ -0,0 +1,25 @@ +import { Elysia } from "elysia"; +import { artistSearchService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +// TODO: add corresponding DTO and response schema +export const artistSearchHandler = new Elysia().get( + "/artists", + async ({ query, status }) => { + const song = await traceTask("artistSearchService.search", async () => { + return await artistSearchService.search(query.q || ""); + }); + return status(200, song); + }, + { + detail: { + summary: "Search for Artists", + description: + "Full-text search across artist names, aliases, and descriptions. Returns a list of matching artists ordered by relevance.", + }, + query: z.object({ + q: z.string().optional(), + }), + } +); diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 216dfd7..d8407cd 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -10,11 +10,35 @@ import { opentelemetry } from "@elysiajs/opentelemetry"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; import { devHandler } from "./handlers"; +import { createOutboxWorker, closeOutboxInfrastructure } from "@cvsa/core"; +import { processOutboxEntry } from "@cvsa/core"; +import { outboxService } from "@cvsa/core"; +import { appLogger } from "@cvsa/logger"; const [host, port] = getBindingInfo(); logStartup(host, port); +const outboxWorker = createOutboxWorker(processOutboxEntry); + +outboxService.recoverStaleEntries().catch((e) => { + appLogger.warn(`Failed to recover stale outbox entries: ${e.message}`); +}); + +process.on("SIGTERM", async () => { + appLogger.info("Received SIGTERM, shutting down gracefully..."); + await outboxWorker.close(); + await closeOutboxInfrastructure(); + process.exit(0); +}); + +process.on("SIGINT", async () => { + appLogger.info("Received SIGINT, shutting down gracefully..."); + await outboxWorker.close(); + await closeOutboxInfrastructure(); + process.exit(0); +}); + export const app = new Elysia({ serve: { hostname: host, diff --git a/apps/backend/tests/artist.test.ts b/apps/backend/tests/artist.test.ts index 93f7f6f..36a1841 100644 --- a/apps/backend/tests/artist.test.ts +++ b/apps/backend/tests/artist.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,19 +6,6 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Artist E2E Tests", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.artist.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - async function getAuthToken() { const signup = await api.v2.user.post({ username: `${Math.random()}`, diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts index 6a9bb96..6ee39aa 100644 --- a/apps/backend/tests/auth.test.ts +++ b/apps/backend/tests/auth.test.ts @@ -1,23 +1,10 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; -import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Registration E2E Tests - POST /v2/user", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - test("should register a new user", async () => { const payload = { username: "new_user", @@ -55,7 +42,7 @@ describe("Registration E2E Tests - POST /v2/user", () => { const payload = { username: "duplicate_user", password: "password123", - email: "first@example.com", + email: "01_first@example.com", }; // Create the first user @@ -64,7 +51,7 @@ describe("Registration E2E Tests - POST /v2/user", () => { // Attempt to register with the same username const { error, status } = await api.v2.user.post({ ...payload, - email: "second@example.com", + email: "01_second@example.com", }); // Should return 409 Conflict @@ -113,16 +100,6 @@ describe("Registration E2E Tests - POST /v2/user", () => { }); describe("Profile E2E Tests - GET /v2/me", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - }); - - afterAll(async () => { - await prisma.$disconnect(); - }); - test("should return user profile with valid token", async () => { // 1. Register to get a token const signup = await api.v2.user.post({ @@ -187,18 +164,6 @@ describe("Profile E2E Tests - GET /v2/me", () => { }); describe("Login E2E Tests - POST /v2/session", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - test("should login with valid credentials", async () => { const signupPayload = { username: "login_user", @@ -277,18 +242,6 @@ describe("Login E2E Tests - POST /v2/session", () => { }); describe("Logout E2E Tests - DELETE /v2/session", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - test("should logout with valid token", async () => { const signupPayload = { username: "logout_user", diff --git a/apps/backend/tests/engine.test.ts b/apps/backend/tests/engine.test.ts index 4c42cc5..54f1f29 100644 --- a/apps/backend/tests/engine.test.ts +++ b/apps/backend/tests/engine.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,19 +6,6 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Engine E2E Tests", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.svsEngine.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - async function getAuthToken() { const signup = await api.v2.user.post({ username: `${Math.random()}`, diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts index 865f284..43e9be3 100644 --- a/apps/backend/tests/song.test.ts +++ b/apps/backend/tests/song.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,20 +6,6 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Song E2E Tests", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.song.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.$disconnect(); - }); - async function getAuthToken() { const signup = await api.v2.user.post({ username: `${Math.random()}`, @@ -122,7 +108,6 @@ describe("Song E2E Tests", () => { expect(status).toBe(201); expect(data).toMatchObject({ - id: expect.any(Number), name: "Lyrics Song", }); diff --git a/apps/backend/tests/songLyrics.test.ts b/apps/backend/tests/songLyrics.test.ts index f2eb306..b55d728 100644 --- a/apps/backend/tests/songLyrics.test.ts +++ b/apps/backend/tests/songLyrics.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { treaty } from "@elysiajs/eden"; import { app } from "@/index"; import { prisma } from "@cvsa/db"; @@ -6,22 +6,6 @@ import { prisma } from "@cvsa/db"; const api = treaty(app); describe("Song Lyrics E2E Tests", () => { - beforeAll(async () => { - await prisma.$connect(); - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.song.deleteMany(); - }); - - afterAll(async () => { - await prisma.session.deleteMany(); - await prisma.user.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.song.deleteMany(); - await prisma.$disconnect(); - }); - async function getAuthToken() { const signup = await api.v2.user.post({ username: `${Math.random()}`, diff --git a/bun.lock b/bun.lock index ac29faa..31ca553 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "cvsa", @@ -57,6 +56,8 @@ "@cvsa/observability": "workspace:*", "@elysiajs/eden": "^1.4.9", "better-auth": "^1.5.6", + "bullmq": "^5", + "ioredis": "^5", "meilisearch": "^0.56.0", "nanoid": "^5.1.7", "remeda": "^2.33.7", @@ -294,10 +295,24 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], @@ -492,7 +507,9 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "bullmq": ["bullmq@5.75.2", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-5GDO2L5OfzogDxzJCeT7icYdI41vQZ5Wuw2z4EqpfPc1m4/gxyg0sEATULGPdR1sMw0kwCPqbehiIPseXxBX9A=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], @@ -510,6 +527,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -522,6 +541,8 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], + "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -638,6 +659,8 @@ "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], + "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -660,10 +683,16 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "meilisearch": ["meilisearch@0.56.0", "", {}, "sha512-kBXba8DcSrLWHYqopCm2JL90oBy97VdIfHkP1ii7/eHufeEEEk9Zu5Vv/IFQpNzmhEpWszVFVlGvZoI6QJeGzA=="], @@ -678,6 +707,10 @@ "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], @@ -686,12 +719,16 @@ "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], + "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -784,6 +821,10 @@ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "remeda": ["remeda@2.33.7", "", {}, "sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -838,6 +879,8 @@ "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -870,6 +913,8 @@ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -892,6 +937,14 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@cvsa/core/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + + "@cvsa/env/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + + "@cvsa/logger/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + + "@cvsa/observability/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@elysiajs/opentelemetry/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], "@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], @@ -910,6 +963,8 @@ "@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "ioredis/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], @@ -920,6 +975,14 @@ "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@cvsa/core/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + + "@cvsa/env/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + + "@cvsa/logger/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + + "@cvsa/observability/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], @@ -966,6 +1029,8 @@ "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "ioredis/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], diff --git a/docker-compose.yml b/docker-compose.yml index 2ec7a0b..803ed39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,16 @@ services: volumes: - meili:/meili_data + redis: + image: redis:7-alpine + container_name: cvsa-redis + restart: always + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis:/data + volumes: data: meili: + redis: diff --git a/locale/modules/backend/messages/en.json b/locale/modules/backend/messages/en.json index 65f64a0..d987e80 100644 --- a/locale/modules/backend/messages/en.json +++ b/locale/modules/backend/messages/en.json @@ -9,5 +9,7 @@ "error.internal": "Internal Error", "error.validation": "Invalid Input", "error.username-taken": "This username is already taken.", - "error.email-taken": "This email is already registered." + "error.email-taken": "This email is already registered.", + "error.artist.notfound": "This artist does not exist.", + "error.engine.notfound": "This engine does not exist." } diff --git a/locale/modules/backend/messages/zh.json b/locale/modules/backend/messages/zh.json index 52acc79..8159c29 100644 --- a/locale/modules/backend/messages/zh.json +++ b/locale/modules/backend/messages/zh.json @@ -9,5 +9,7 @@ "error.internal": "服务器内部错误", "error.validation": "输入信息有误", "error.username-taken": "此用户名已被注册", - "error.email-taken": "此邮箱已被注册" + "error.email-taken": "此邮箱已被注册", + "error.artist.notfound": "该创作者不存在", + "error.engine.notfound": "该引擎不存在" } diff --git a/package.json b/package.json index 5fbbf28..94e7b5e 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,8 @@ "lint:fix": "bunx --bun biome check --write --unsafe .", "format": "bunx --bun biome format --write .", "ci": "bunx --bun biome ci .", - "test": "bunx --bun turbo run test", - "test:backend": "bunx --bun turbo run test --filter=@cvsa/backend", - "test:core": "bunx --bun turbo run test --filter=@cvsa/core", - "test:coverage": "bunx --bun turbo run test:coverage", + "test": "bunx --bun turbo run test:reset && bunx --bun turbo run test", + "test:coverage": "bunx --bun turbo run test:reset && bunx --bun turbo run test:coverage", "typecheck": "bunx --bun turbo typecheck", "setup": "bunx --bun turbo setup", "setup:ci": "bunx --bun turbo setup:ci" diff --git a/packages/core/bunfig.toml b/packages/core/bunfig.toml index 3fc94a4..b97d83e 100644 --- a/packages/core/bunfig.toml +++ b/packages/core/bunfig.toml @@ -8,6 +8,8 @@ coveragePathIgnorePatterns = [ "src/error/**", "**/**.interface.ts", "src/utils/randomId.ts", + "src/utils/BaseRepository.ts", "src/types/**", + "src/outbox/queue.ts", "**/observability/src/**", ] \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 1e3e5af..2458fff 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,6 +7,8 @@ "scripts": { "lint": "bunx --bun biome lint", "test": "bun test --concurrent", + "test:reset": "NODE_ENV=test bun scripts/cleanUpSearchIndex.ts", + "test:safe": "bun run test:reset && bun run test", "test:coverage": "bun test --coverage", "test:unit": "bun test tests/unit --concurrent", "test:integration": "bun test tests/integration --concurrent", @@ -30,6 +32,8 @@ "@cvsa/observability": "workspace:*", "@elysiajs/eden": "^1.4.9", "better-auth": "^1.5.6", + "bullmq": "^5", + "ioredis": "^5", "meilisearch": "^0.56.0", "nanoid": "^5.1.7", "remeda": "^2.33.7", diff --git a/packages/core/scripts/cleanUpSearchIndex.ts b/packages/core/scripts/cleanUpSearchIndex.ts new file mode 100644 index 0000000..b6f1405 --- /dev/null +++ b/packages/core/scripts/cleanUpSearchIndex.ts @@ -0,0 +1,13 @@ +import { searchManager } from "../src"; +import { INDEX_SETTINGS } from "../src/search/config"; + +await searchManager.clearAllIndex(); +const langs = ["en", "zh", "ja"]; +const keys = Object.keys(INDEX_SETTINGS); +for (const lang of langs) { + for (const key of keys) { + await searchManager.createIndex(`${key}_${lang}`); + } +} + +process.exit(0); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ae044e3..a57b424 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,3 +3,4 @@ export * from "./search"; export * from "./utils"; export * from "./types"; export * from "./error"; +export * from "./outbox"; diff --git a/packages/core/src/modules/catalog/artist/container.ts b/packages/core/src/modules/catalog/artist/container.ts index f8c2c71..6199de6 100644 --- a/packages/core/src/modules/catalog/artist/container.ts +++ b/packages/core/src/modules/catalog/artist/container.ts @@ -1,6 +1,17 @@ import { prisma } from "@cvsa/db"; import { ArtistRepository } from "./repository"; import { ArtistService } from "./service"; +import { ArtistSearchService, searchManager } from "../../../search"; +import { treaty } from "@elysiajs/eden"; +import type { EmbeddingApp } from "@cvsa/embedding"; +import { outboxService } from "../../outbox/container"; + +const embeddingManager = treaty("localhost:14900"); export const artistRepository = new ArtistRepository(prisma); -export const artistService = new ArtistService(artistRepository); +export const artistSearchService = new ArtistSearchService( + artistRepository, + searchManager, + embeddingManager +); +export const artistService = new ArtistService(artistRepository, outboxService); diff --git a/packages/core/src/modules/catalog/artist/repository.ts b/packages/core/src/modules/catalog/artist/repository.ts index 0897f52..279796d 100644 --- a/packages/core/src/modules/catalog/artist/repository.ts +++ b/packages/core/src/modules/catalog/artist/repository.ts @@ -1,4 +1,6 @@ import type { PrismaClient } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; +import { BaseRepository } from "../../../utils/BaseRepository"; import type { CreateArtistRequestDto, ArtistId, @@ -6,15 +8,16 @@ import type { ArtistDetailsResponseDto, } from "./dto"; import type { IArtistRepository } from "./repository.interface"; -import { transformPrismaResult, type TxClient } from "@cvsa/db"; -export class ArtistRepository implements IArtistRepository { - constructor(private readonly prisma: PrismaClient) {} +export class ArtistRepository extends BaseRepository implements IArtistRepository { + constructor(private readonly prisma: PrismaClient) { + super(); + } async getById(id: ArtistId, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.artist.findFirst({ + return this.query("db.artist.getById", () => + client.artist.findFirst({ where: { id, deletedAt: null }, omit: { deletedAt: true }, }) @@ -23,8 +26,8 @@ export class ArtistRepository implements IArtistRepository { async getDetailsById(id: ArtistId, tx?: TxClient): Promise { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.artist.findFirst({ + return this.query("db.artist.getDetailsById", () => + client.artist.findFirst({ where: { id, deletedAt: null }, omit: { deletedAt: true }, }) @@ -33,9 +36,8 @@ export class ArtistRepository implements IArtistRepository { async create(input: CreateArtistRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - - return transformPrismaResult( - await client.artist.create({ + return this.query("db.artist.create", () => + client.artist.create({ data: input, omit: { deletedAt: true }, }) @@ -44,9 +46,8 @@ export class ArtistRepository implements IArtistRepository { async update(id: ArtistId, input: UpdateArtistRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - - return transformPrismaResult( - await client.artist.update({ + return this.query("db.artist.update", () => + client.artist.update({ where: { id }, data: input, omit: { deletedAt: true }, @@ -56,9 +57,11 @@ export class ArtistRepository implements IArtistRepository { async softDelete(id: ArtistId, tx?: TxClient) { const client = tx ?? this.prisma; - await client.artist.update({ - where: { id }, - data: { deletedAt: new Date() }, - }); + await this.query("db.artist.softDelete", () => + client.artist.update({ + where: { id }, + data: { deletedAt: new Date() }, + }) + ); } } diff --git a/packages/core/src/modules/catalog/artist/service.ts b/packages/core/src/modules/catalog/artist/service.ts index d688934..854c873 100644 --- a/packages/core/src/modules/catalog/artist/service.ts +++ b/packages/core/src/modules/catalog/artist/service.ts @@ -1,4 +1,7 @@ -import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import type { OutboxService } from "../../outbox/service"; +import { AppError } from "../../../error/AppError"; +import type { IServiceWithGetDetails } from "../../../types/service"; +import { prisma } from "@cvsa/db"; import type { ArtistDetailsResponseDto, ArtistId, @@ -7,25 +10,37 @@ import type { ArtistResponseDto, } from "./dto"; import type { IArtistRepository } from "./repository.interface"; -import { traceTask } from "@cvsa/observability"; export class ArtistService implements IServiceWithGetDetails { - constructor(private readonly repository: IArtistRepository) {} + constructor( + private readonly repository: IArtistRepository, + private readonly outbox: OutboxService + ) {} async getDetails(id: ArtistId) { - return traceTask("db findOne artist", async () => { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.artist.notfound", "NOT_FOUND", 404); - } - return result; - }); + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.artist.notfound", "NOT_FOUND", 404); + } + return result; } async create(input: CreateArtistRequestDto): Promise { - return traceTask("db create artist", async () => { - return await this.repository.create(input); + const { artist, entry } = await prisma.$transaction(async (tx) => { + const artist = await this.repository.create(input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "artist", + aggregateId: artist.id, + eventType: "artist.created", + }, + tx + ); + return { artist, entry }; }); + + await this.outbox.enqueue(entry); + return artist; } async update(id: ArtistId, input: UpdateArtistRequestDto): Promise { @@ -33,9 +48,22 @@ export class ArtistService implements IServiceWithGetDetails { - return await this.repository.update(id, input); + + const { artist, entry } = await prisma.$transaction(async (tx) => { + const artist = await this.repository.update(id, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "artist", + aggregateId: id, + eventType: "artist.updated", + }, + tx + ); + return { artist, entry }; }); + + await this.outbox.enqueue(entry); + return artist; } async delete(id: ArtistId): Promise { @@ -43,8 +71,19 @@ export class ArtistService implements IServiceWithGetDetails { - return await this.repository.softDelete(id); + + const entry = await prisma.$transaction(async (tx) => { + await this.repository.softDelete(id, tx); + return await this.outbox.createEntry( + { + aggregateType: "artist", + aggregateId: id, + eventType: "artist.deleted", + }, + tx + ); }); + + await this.outbox.enqueue(entry); } } diff --git a/packages/core/src/modules/catalog/engine/repository.ts b/packages/core/src/modules/catalog/engine/repository.ts index 23670e2..27358f8 100644 --- a/packages/core/src/modules/catalog/engine/repository.ts +++ b/packages/core/src/modules/catalog/engine/repository.ts @@ -1,4 +1,6 @@ import type { PrismaClient } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; +import { BaseRepository } from "../../../utils/BaseRepository"; import type { CreateEngineRequestDto, EngineId, @@ -6,15 +8,16 @@ import type { EngineDetailsResponseDto, } from "./dto"; import type { IEngineRepository } from "./repository.interface"; -import { transformPrismaResult, type TxClient } from "@cvsa/db"; -export class EngineRepository implements IEngineRepository { - constructor(private readonly prisma: PrismaClient) {} +export class EngineRepository extends BaseRepository implements IEngineRepository { + constructor(private readonly prisma: PrismaClient) { + super(); + } async getById(id: EngineId, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.svsEngine.findFirst({ + return this.query("db.engine.getById", () => + client.svsEngine.findFirst({ where: { id, deletedAt: null }, omit: { deletedAt: true }, }) @@ -23,8 +26,8 @@ export class EngineRepository implements IEngineRepository { async getDetailsById(id: EngineId, tx?: TxClient): Promise { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.svsEngine.findFirst({ + return this.query("db.engine.getDetailsById", () => + client.svsEngine.findFirst({ where: { id, deletedAt: null }, omit: { deletedAt: true }, }) @@ -33,9 +36,8 @@ export class EngineRepository implements IEngineRepository { async create(input: CreateEngineRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - - return transformPrismaResult( - await client.svsEngine.create({ + return this.query("db.engine.create", () => + client.svsEngine.create({ data: input, omit: { deletedAt: true }, }) @@ -44,9 +46,8 @@ export class EngineRepository implements IEngineRepository { async update(id: EngineId, input: UpdateEngineRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - - return transformPrismaResult( - await client.svsEngine.update({ + return this.query("db.engine.update", () => + client.svsEngine.update({ where: { id }, data: input, omit: { deletedAt: true }, @@ -56,9 +57,11 @@ export class EngineRepository implements IEngineRepository { async softDelete(id: EngineId, tx?: TxClient) { const client = tx ?? this.prisma; - await client.svsEngine.update({ - where: { id }, - data: { deletedAt: new Date() }, - }); + await this.query("db.engine.softDelete", () => + client.svsEngine.update({ + where: { id }, + data: { deletedAt: new Date() }, + }) + ); } } diff --git a/packages/core/src/modules/catalog/engine/service.ts b/packages/core/src/modules/catalog/engine/service.ts index 6f46ea5..c463249 100644 --- a/packages/core/src/modules/catalog/engine/service.ts +++ b/packages/core/src/modules/catalog/engine/service.ts @@ -1,4 +1,5 @@ -import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import { AppError } from "../../../error/AppError"; +import type { IServiceWithGetDetails } from "../../../types/service"; import type { EngineDetailsResponseDto, EngineId, diff --git a/packages/core/src/modules/catalog/song/container.ts b/packages/core/src/modules/catalog/song/container.ts index ee6bdf9..86b9f0d 100644 --- a/packages/core/src/modules/catalog/song/container.ts +++ b/packages/core/src/modules/catalog/song/container.ts @@ -2,13 +2,16 @@ import { prisma } from "@cvsa/db"; import { SongRepository } from "./repository"; import { SongService } from "./service"; import { SongSearchService, searchManager } from "../../../search"; -import type { EmbeddingApp } from "@cvsa/embedding"; import { treaty } from "@elysiajs/eden"; -export const embeddingManager = treaty("localhost:14900"); +import type { EmbeddingApp } from "@cvsa/embedding"; +import { outboxService } from "../../outbox/container"; + +const embeddingManager = treaty("localhost:14900"); + export const songRepository = new SongRepository(prisma); export const songSearchService = new SongSearchService( songRepository, searchManager, embeddingManager ); -export const songService = new SongService(songRepository, songSearchService); +export const songService = new SongService(songRepository, outboxService); diff --git a/packages/core/src/modules/catalog/song/repository.ts b/packages/core/src/modules/catalog/song/repository.ts index 7ac77bc..3de15aa 100644 --- a/packages/core/src/modules/catalog/song/repository.ts +++ b/packages/core/src/modules/catalog/song/repository.ts @@ -1,4 +1,6 @@ import type { PrismaClient } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; +import { BaseRepository } from "../../../utils/BaseRepository"; import type { CreateSongRequestDto, SongId, @@ -8,15 +10,16 @@ import type { SongLyricsUpdateRequestDto, } from "./dto"; import type { ISongRepository } from "./repository.interface"; -import { transformPrismaResult, type TxClient } from "@cvsa/db"; -export class SongRepository implements ISongRepository { - constructor(private readonly prisma: PrismaClient) {} +export class SongRepository extends BaseRepository implements ISongRepository { + constructor(private readonly prisma: PrismaClient) { + super(); + } async getById(id: SongId, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.song.findFirst({ + return this.query("db.song.getById", () => + client.song.findFirst({ where: { id, deletedAt: null }, omit: { deletedAt: true }, }) @@ -25,8 +28,8 @@ export class SongRepository implements ISongRepository { async getDetailsById(id: SongId, tx?: TxClient): Promise { const client = tx ?? this.prisma; - const data = transformPrismaResult( - await client.song.findFirst({ + const data = await this.query("db.song.getDetailsById", () => + client.song.findFirst({ where: { id, deletedAt: null }, include: { performances: { @@ -84,8 +87,8 @@ export class SongRepository implements ISongRepository { const client = tx ?? this.prisma; const { performances, creations, lyrics, ...songData } = input; - return transformPrismaResult( - await client.song.create({ + return this.query("db.song.create", () => + client.song.create({ data: { ...songData, performances: performances && { @@ -111,8 +114,8 @@ export class SongRepository implements ISongRepository { async update(id: SongId, input: UpdateSongRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.song.update({ + return this.query("db.song.update", () => + client.song.update({ where: { id }, data: input, omit: { @@ -124,14 +127,19 @@ export class SongRepository implements ISongRepository { async softDelete(id: SongId, tx?: TxClient) { const client = tx ?? this.prisma; - await client.song.update({ where: { id }, data: { deletedAt: new Date() } }); + await this.query("db.song.softDelete", () => + client.song.update({ + where: { id }, + data: { deletedAt: new Date() }, + }) + ); } async createLyrics(id: SongId, input: SongLyricsCreateRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.lyrics.create({ + return this.query("db.song.createLyrics", () => + client.lyrics.create({ data: { songId: id, ...input, @@ -146,8 +154,8 @@ export class SongRepository implements ISongRepository { async getLyricsBySongId(id: SongId, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.lyrics.findMany({ + return this.query("db.song.getLyricsBySongId", () => + client.lyrics.findMany({ where: { songId: id, deletedAt: null }, omit: { deletedAt: true, songId: true }, }) @@ -157,8 +165,8 @@ export class SongRepository implements ISongRepository { async getLyricById(lyricId: number, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.lyrics.findFirst({ + return this.query("db.song.getLyricById", () => + client.lyrics.findFirst({ where: { id: lyricId, deletedAt: null }, omit: { deletedAt: true, songId: true }, }) @@ -168,8 +176,8 @@ export class SongRepository implements ISongRepository { async updateLyric(lyricId: number, input: SongLyricsUpdateRequestDto, tx?: TxClient) { const client = tx ?? this.prisma; - return transformPrismaResult( - await client.lyrics.update({ + return this.query("db.song.updateLyric", () => + client.lyrics.update({ where: { id: lyricId }, data: input, omit: { deletedAt: true, songId: true }, @@ -180,9 +188,11 @@ export class SongRepository implements ISongRepository { async softDeleteLyric(lyricId: number, tx?: TxClient) { const client = tx ?? this.prisma; - await client.lyrics.update({ - where: { id: lyricId }, - data: { deletedAt: new Date() }, - }); + await this.query("db.song.softDeleteLyric", () => + client.lyrics.update({ + where: { id: lyricId }, + data: { deletedAt: new Date() }, + }) + ); } } diff --git a/packages/core/src/modules/catalog/song/service.ts b/packages/core/src/modules/catalog/song/service.ts index 8e9a6ba..711a7a3 100644 --- a/packages/core/src/modules/catalog/song/service.ts +++ b/packages/core/src/modules/catalog/song/service.ts @@ -1,5 +1,6 @@ -import type { SongSearchService } from "@cvsa/core/internal"; +import type { OutboxService } from "@cvsa/core/internal"; import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; +import { prisma } from "@cvsa/db"; import type { SongDetailsResponseDto, SongId, @@ -12,35 +13,37 @@ import type { SongLyricsListResponseDto, } from "./dto"; import type { ISongRepository } from "./repository.interface"; -import { traceTask } from "@cvsa/observability"; -import { appLogger } from "@cvsa/logger"; export class SongService implements IServiceWithGetDetails { constructor( private readonly repository: ISongRepository, - private readonly search: SongSearchService + private readonly outbox: OutboxService ) {} async getDetails(id: SongId) { - return traceTask("db findOne song", async () => { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - return result; - }); + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.song.notfound", "NOT_FOUND", 404); + } + return result; } async create(input: CreateSongRequestDto): Promise { - const result = await traceTask("db create song", async () => { - return await this.repository.create(input); + const { song, entry } = await prisma.$transaction(async (tx) => { + const song = await this.repository.create(input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: song.id, + eventType: "song.created", + }, + tx + ); + return { song, entry }; }); - await traceTask("sync search index", async () => { - this.search.sync(result.id).catch((e) => { - appLogger.warn(Bun.inspect(e)); - }); - }); - return result; + + await this.outbox.enqueue(entry); + return song; } async update(id: SongId, input: UpdateSongRequestDto): Promise { @@ -48,16 +51,22 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.update(id, input); - }); - await traceTask("sync search index", async () => { - this.search.sync(id).catch((e) => { - // TODO: Should we mark this as dirty and sync it later? - appLogger.warn(Bun.inspect(e)); - }); + + const { song, entry } = await prisma.$transaction(async (tx) => { + const song = await this.repository.update(id, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: id, + eventType: "song.updated", + }, + tx + ); + return { song, entry }; }); - return result; + + await this.outbox.enqueue(entry); + return song; } async delete(id: SongId): Promise { @@ -65,14 +74,20 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.softDelete(id); - }); - await traceTask("sync search index", async () => { - this.search.sync(id).catch((e) => { - appLogger.warn(Bun.inspect(e)); - }); + + const entry = await prisma.$transaction(async (tx) => { + await this.repository.softDelete(id, tx); + return await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: id, + eventType: "song.deleted", + }, + tx + ); }); + + await this.outbox.enqueue(entry); } async listLyrics(id: SongId): Promise { @@ -80,9 +95,7 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.getLyricsBySongId(id); - }); + return this.repository.getLyricsBySongId(id); } async getLyric(id: SongId, lyricId: number): Promise { @@ -90,9 +103,7 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.getLyricById(lyricId); - }); + const lyric = await this.repository.getLyricById(lyricId); if (lyric === null) { throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); } @@ -107,15 +118,22 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.createLyrics(id, input); - }); - await traceTask("sync search index", async () => { - this.search.sync(id).catch((e) => { - appLogger.warn(Bun.inspect(e)); - }); + + const { lyric, entry } = await prisma.$transaction(async (tx) => { + const lyric = await this.repository.createLyrics(id, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: id, + eventType: "song.lyric_created", + }, + tx + ); + return { lyric, entry }; }); - return result; + + await this.outbox.enqueue(entry); + return lyric; } async updateLyric( @@ -131,15 +149,22 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.updateLyric(lyricId, input); - }); - await traceTask("sync search index", async () => { - this.search.sync(id).catch((e) => { - appLogger.warn(Bun.inspect(e)); - }); + + const { lyric: updated, entry } = await prisma.$transaction(async (tx) => { + const lyric = await this.repository.updateLyric(lyricId, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: id, + eventType: "song.lyric_updated", + }, + tx + ); + return { lyric, entry }; }); - return result; + + await this.outbox.enqueue(entry); + return updated; } async deleteLyric(id: SongId, lyricId: number): Promise { @@ -151,13 +176,19 @@ export class SongService implements IServiceWithGetDetails { - return await this.repository.softDeleteLyric(lyricId); - }); - await traceTask("sync search index", async () => { - this.search.sync(id).catch((e) => { - appLogger.warn(Bun.inspect(e)); - }); + + const entry = await prisma.$transaction(async (tx) => { + await this.repository.softDeleteLyric(lyricId, tx); + return await this.outbox.createEntry( + { + aggregateType: "song", + aggregateId: id, + eventType: "song.lyric_deleted", + }, + tx + ); }); + + await this.outbox.enqueue(entry); } } diff --git a/packages/core/src/modules/index.ts b/packages/core/src/modules/index.ts index 7ac6bfd..8c4f84d 100644 --- a/packages/core/src/modules/index.ts +++ b/packages/core/src/modules/index.ts @@ -1,2 +1,3 @@ export * from "./catalog"; export * from "./auth"; +export * from "./outbox"; diff --git a/packages/core/src/modules/outbox/container.ts b/packages/core/src/modules/outbox/container.ts new file mode 100644 index 0000000..90f4d62 --- /dev/null +++ b/packages/core/src/modules/outbox/container.ts @@ -0,0 +1,6 @@ +import { prisma } from "@cvsa/db"; +import { OutboxRepository } from "./repository"; +import { OutboxService } from "./service"; + +export const outboxRepository = new OutboxRepository(prisma); +export const outboxService = new OutboxService(outboxRepository); diff --git a/packages/core/src/modules/outbox/dto.ts b/packages/core/src/modules/outbox/dto.ts new file mode 100644 index 0000000..95438b0 --- /dev/null +++ b/packages/core/src/modules/outbox/dto.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +export const CreateOutboxEntrySchema = z.object({ + aggregateType: z.string(), + aggregateId: z.number().int(), + eventType: z.string(), + payload: z.record(z.string(), z.unknown()).optional(), +}); + +export const PendingOutboxQuerySchema = z.object({ + limit: z.number().int().positive().default(100), +}); + +export type CreateOutboxEntryDto = { + aggregateType: string; + aggregateId: number; + eventType: string; + payload?: Record; +}; + +export type OutboxEntryDto = { + id: number; + aggregateType: string; + aggregateId: number; + eventType: string; + payload: unknown; + status: "PENDING" | "PROCESSING" | "PROCESSED" | "FAILED"; + retryCount: number; + lastError: string | null; + nextRetryAt: string | null; + createdAt: string; + processedAt: string | null; +}; + +export type PendingOutboxQueryDto = z.infer; diff --git a/packages/core/src/modules/outbox/index.ts b/packages/core/src/modules/outbox/index.ts new file mode 100644 index 0000000..132cd4a --- /dev/null +++ b/packages/core/src/modules/outbox/index.ts @@ -0,0 +1,5 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./repository.interface"; +export * from "./service"; +export * from "./container"; diff --git a/packages/core/src/modules/outbox/repository.interface.ts b/packages/core/src/modules/outbox/repository.interface.ts new file mode 100644 index 0000000..2317b39 --- /dev/null +++ b/packages/core/src/modules/outbox/repository.interface.ts @@ -0,0 +1,15 @@ +import type { TxClient } from "@cvsa/db"; +import type { CreateOutboxEntryDto, OutboxEntryDto, PendingOutboxQueryDto } from "./dto"; + +export abstract class IOutboxRepository { + abstract create(input: CreateOutboxEntryDto, tx?: TxClient): Promise; + abstract findPending(query: PendingOutboxQueryDto): Promise; + abstract markProcessing(id: number, tx?: TxClient): Promise; + abstract markProcessed(id: number, tx?: TxClient): Promise; + abstract markFailed( + id: number, + lastError: string, + nextRetryAt: Date, + tx?: TxClient + ): Promise; +} diff --git a/packages/core/src/modules/outbox/repository.ts b/packages/core/src/modules/outbox/repository.ts new file mode 100644 index 0000000..aa5d46e --- /dev/null +++ b/packages/core/src/modules/outbox/repository.ts @@ -0,0 +1,71 @@ +import type { PrismaClient } from "@cvsa/db"; +import { transformPrismaResult, type TxClient } from "@cvsa/db"; +import type { CreateOutboxEntryDto, PendingOutboxQueryDto } from "./dto"; +import type { IOutboxRepository } from "./repository.interface"; + +export class OutboxRepository implements IOutboxRepository { + constructor(private readonly prisma: PrismaClient) {} + + async create(input: CreateOutboxEntryDto, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.outbox.create({ + data: { + aggregateType: input.aggregateType, + aggregateId: input.aggregateId, + eventType: input.eventType, + payload: input.payload ?? null, + }, + }) + ); + } + + async findPending(query: PendingOutboxQueryDto) { + const client = this.prisma; + return transformPrismaResult( + await client.outbox.findMany({ + where: { + status: "PENDING", + OR: [{ nextRetryAt: { lte: new Date() } }, { nextRetryAt: null }], + }, + orderBy: { createdAt: "asc" }, + take: query.limit, + }) + ); + } + + async markProcessing(id: number, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.outbox.update({ + where: { id }, + data: { status: "PROCESSING" }, + }) + ); + } + + async markProcessed(id: number, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.outbox.update({ + where: { id }, + data: { status: "PROCESSED", processedAt: new Date() }, + }) + ); + } + + async markFailed(id: number, lastError: string, nextRetryAt: Date, tx?: TxClient) { + const client = tx ?? this.prisma; + return transformPrismaResult( + await client.outbox.update({ + where: { id }, + data: { + status: "FAILED", + lastError, + nextRetryAt, + retryCount: { increment: 1 }, + }, + }) + ); + } +} diff --git a/packages/core/src/modules/outbox/service.ts b/packages/core/src/modules/outbox/service.ts new file mode 100644 index 0000000..14a30de --- /dev/null +++ b/packages/core/src/modules/outbox/service.ts @@ -0,0 +1,78 @@ +import type { Queue } from "bullmq"; +import type { IOutboxRepository } from "./repository.interface"; +import type { CreateOutboxEntryDto, OutboxEntryDto, PendingOutboxQueryDto } from "./dto"; +import { Prisma, type TxClient } from "@cvsa/db"; +import { outboxQueue } from "../../outbox/queue"; +import { appLogger } from "@cvsa/logger"; +import { traceTask } from "@cvsa/observability"; + +const MAX_RETRIES = 5; + +export class OutboxService { + private readonly queue: Queue; + + constructor( + private readonly repository: IOutboxRepository, + queue?: Queue + ) { + this.queue = queue ?? outboxQueue; + } + + async createEntry(input: CreateOutboxEntryDto, tx: TxClient): Promise { + return traceTask("outbox.createEntry", () => this.repository.create(input, tx)); + } + + async enqueue(entry: OutboxEntryDto): Promise { + await traceTask("outbox.enqueue", async () => { + await this.queue.add(`outbox-${entry.aggregateType}-${entry.aggregateId}`, entry, { + jobId: `outbox-${entry.id}`, + attempts: MAX_RETRIES, + backoff: { type: "exponential", delay: 1000 }, + }); + }); + } + + async processEntry( + id: number, + processor: (aggregateType: string, aggregateId: number, eventType: string) => Promise + ): Promise { + try { + const entry = await this.repository.markProcessing(id); + try { + await processor(entry.aggregateType, entry.aggregateId, entry.eventType); + await this.repository.markProcessed(id); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + appLogger.warn(`Outbox entry ${id} processing failed: ${errorMessage}`); + if (entry.retryCount >= MAX_RETRIES - 1) { + await this.repository.markFailed( + id, + errorMessage, + new Date(Date.now() + 3600_000) + ); + } else { + throw error; + } + } + } catch (error) { + // Handle case where outbox entry no longer exists (already processed or cleaned up) + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { + appLogger.debug(`Outbox entry ${id} not found, skipping processing`); + return; + } + throw error; + } + } + + async recoverStaleEntries(limit: number = 100): Promise { + const query: PendingOutboxQueryDto = { limit }; + const entries = await this.repository.findPending(query); + for (const entry of entries) { + await this.queue.add(`outbox-recover-${entry.id}`, entry, { + jobId: `outbox-recover-${entry.id}`, + attempts: MAX_RETRIES, + backoff: { type: "exponential", delay: 1000 }, + }); + } + } +} diff --git a/packages/core/src/outbox/index.ts b/packages/core/src/outbox/index.ts new file mode 100644 index 0000000..1336656 --- /dev/null +++ b/packages/core/src/outbox/index.ts @@ -0,0 +1,2 @@ +export * from "./queue"; +export * from "./processor"; diff --git a/packages/core/src/outbox/processor.ts b/packages/core/src/outbox/processor.ts new file mode 100644 index 0000000..de224e7 --- /dev/null +++ b/packages/core/src/outbox/processor.ts @@ -0,0 +1,48 @@ +import type { Job } from "bullmq"; +import type { OutboxEntryDto, OutboxService } from "@cvsa/core/internal"; +import { songSearchService, artistSearchService } from "@cvsa/core/internal"; +import { outboxService } from "../modules/outbox/container"; +import { appLogger } from "@cvsa/logger"; + +export interface OutboxProcessorDeps { + outboxService: OutboxService; + searchServices: Record }>; +} + +export function createOutboxProcessor( + deps: OutboxProcessorDeps +): (job: Job) => Promise { + return async function processOutboxEntry(job: Job): Promise { + const entry = job.data; + + await deps.outboxService.processEntry( + entry.id, + async (aggregateType, aggregateId, eventType) => { + appLogger.debug( + `processing outbox entry with ID ${aggregateId}, type ${aggregateType}`, + { + aggregateId, + aggregateType, + eventType, + } + ); + + const searchService = deps.searchServices[aggregateType]; + if (!searchService) { + appLogger.warn(`Unknown aggregate type: ${aggregateType}`); + throw new Error(`Unknown aggregate type: ${aggregateType}`); + } + + await searchService.sync(aggregateId); + } + ); + }; +} + +export const processOutboxEntry = createOutboxProcessor({ + outboxService, + searchServices: { + song: songSearchService, + artist: artistSearchService, + }, +}); diff --git a/packages/core/src/outbox/queue.ts b/packages/core/src/outbox/queue.ts new file mode 100644 index 0000000..7922aac --- /dev/null +++ b/packages/core/src/outbox/queue.ts @@ -0,0 +1,61 @@ +import { Queue, Worker, type Job } from "bullmq"; +import IORedis from "ioredis"; +import { env } from "@cvsa/env"; +import { appLogger } from "@cvsa/logger"; +import type { OutboxEntryDto } from "@cvsa/core/internal"; + +const QUEUE_NAME = "outbox"; + +let connection: IORedis | undefined; + +function getConnection(): IORedis { + if (!connection) { + connection = new IORedis(env.REDIS_URL, { + maxRetriesPerRequest: null, + }); + } + return connection; +} + +export const outboxQueue = new Queue(QUEUE_NAME, { + connection: getConnection(), + defaultJobOptions: { + attempts: 5, + backoff: { + type: "exponential", + delay: 1000, + }, + removeOnComplete: true, + removeOnFail: { + age: 24 * 3600, + count: 1000, + }, + }, +}); + +export function createOutboxWorker( + processor: (job: Job) => Promise +): Worker { + const worker = new Worker(QUEUE_NAME, processor, { + connection: getConnection(), + concurrency: 5, + }); + + worker.on("completed", (job) => { + appLogger.info(`Outbox job ${job.id} completed`); + }); + + worker.on("failed", (job, err) => { + appLogger.error(`Outbox job ${job?.id} failed: ${err.message}`); + }); + + return worker; +} + +export async function closeOutboxInfrastructure(): Promise { + await outboxQueue.close(); + if (connection) { + await connection.quit(); + connection = undefined; + } +} diff --git a/packages/core/src/search/catalog/artist.ts b/packages/core/src/search/catalog/artist.ts new file mode 100644 index 0000000..edc5e7d --- /dev/null +++ b/packages/core/src/search/catalog/artist.ts @@ -0,0 +1,105 @@ +import { ISearchService } from "../interface"; +import { unique, keys } from "remeda"; +import type { ArtistDetailsResponseDto } from "../../modules"; +import { appLogger } from "@cvsa/logger"; + +export interface ArtistSearchIndex { + id: number; + name?: string; + description?: string; + aliases?: string[]; + _vectors?: { + "potion-multilingual-128M": number[] | null; + }; +} + +export class ArtistSearchService extends ISearchService { + private async getDocument( + artist: ArtistDetailsResponseDto, + language: string + ): Promise { + const getDesc = () => { + if (language === artist.language) return artist.description; + return artist.localizedDescriptions?.[language]; + }; + const getName = () => { + if (language === artist.language) return artist.name; + return artist.localizedNames?.[language]; + }; + + const vectors = await this.embeddingManager.embeddings.post({ + texts: [ + `Name: ${getName() ?? ""} +Description: ${getDesc() ?? ""} +Name Aliases: ${artist.aliases.join(", ")} +`, + ], + }); + return { + id: artist.id, + name: getName() ?? undefined, + aliases: artist.aliases, + description: getDesc() ?? undefined, + _vectors: vectors.data?.embeddings[0] + ? { + "potion-multilingual-128M": vectors.data?.embeddings[0], + } + : { + "potion-multilingual-128M": null, + }, + }; + } + + public async sync(id: number) { + if (!this.searchManager) { + appLogger.warn("Search service not available"); + return; + } + const artist = await this.repository.getDetailsById(id); + + if (!artist) { + const indexesToBeDeleted = + await this.searchManager.getLocalizedIndexesOfEntity("artist"); + for (const index of indexesToBeDeleted) { + const adminIndex = await this.searchManager.getAdminIndex(index); + const task = await adminIndex.deleteDocument(id); + await this.searchManager.waitForTask(task.taskUid); + } + return; + } + + const languages = unique([ + ...keys(artist.localizedNames ?? []), + ...keys(artist.localizedDescriptions ?? []), + artist.language, + ]); + for (const language of languages) { + const indexUid = `artist_${language}`; + const index = await this.searchManager.getAdminIndex(indexUid); + const document = await this.getDocument(artist, language); + const task = await index.addDocuments([document], { + primaryKey: "id", + }); + await this.searchManager.waitForTask(task.taskUid); + } + } + + public async search(query: string, language: string = "zh") { + if (!this.searchManager) { + throw new Error("Search or embedding service not available"); + } + + const index = await this.searchManager.getSearchIndex(`artist_${language}`); + const embeddingResponse = await this.embeddingManager.embeddings.post({ + texts: [query], + }); + return index.search(query, { + vector: embeddingResponse?.data?.embeddings[0], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + } +} diff --git a/packages/core/src/search/catalog/index.ts b/packages/core/src/search/catalog/index.ts index 153b6ce..9d9a57c 100644 --- a/packages/core/src/search/catalog/index.ts +++ b/packages/core/src/search/catalog/index.ts @@ -1 +1,2 @@ export * from "./song"; +export * from "./artist"; diff --git a/packages/core/src/search/catalog/song.ts b/packages/core/src/search/catalog/song.ts index d67ed7c..5449fb6 100644 --- a/packages/core/src/search/catalog/song.ts +++ b/packages/core/src/search/catalog/song.ts @@ -19,11 +19,10 @@ export interface SongSearchIndex { publishedAt?: number; bilibiliViews?: number; _vectors?: { - "potion-multilingual-128M": number[]; + "potion-multilingual-128M": number[] | null; }; } -// TODO: Integration test export class SongSearchService extends ISearchService { private async getDocument( song: SongDetailsResponseDto, @@ -79,12 +78,13 @@ Artists: ${getArtists().join(", ")} .map((item) => item.engine ?? undefined) .filter(Boolean) as string[], publishedAt: song.publishedAt ? new Date(song.publishedAt).getTime() : undefined, - // TODO: Error handling. _vectors: vectors.data?.embeddings[0] ? { "potion-multilingual-128M": vectors.data?.embeddings[0], } - : undefined, + : { + "potion-multilingual-128M": null, + }, }; } @@ -130,12 +130,15 @@ Artists: ${getArtists().join(", ")} const embeddingResponse = await this.embeddingManager.embeddings.post({ texts: [query], }); + const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0; return index.search(query, { vector: embeddingResponse?.data?.embeddings[0], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, + hybrid: embeddingAvailable + ? { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + } + : undefined, showRankingScore: true, }); } diff --git a/packages/core/src/search/config.ts b/packages/core/src/search/config.ts index 2f8d436..410db0a 100644 --- a/packages/core/src/search/config.ts +++ b/packages/core/src/search/config.ts @@ -47,4 +47,14 @@ export const INDEX_SETTINGS: Record = { // }, }, }, + artist: { + searchableAttributes: ["name", "description", "aliases"], + rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], + embedders: { + "potion-multilingual-128M": { + source: "userProvided", + dimensions: 256, + }, + }, + }, }; diff --git a/packages/core/src/search/manager.ts b/packages/core/src/search/manager.ts index d9ab328..b252098 100644 --- a/packages/core/src/search/manager.ts +++ b/packages/core/src/search/manager.ts @@ -1,4 +1,4 @@ -import { MeiliSearch, type Settings, type RecordAny } from "meilisearch"; +import { MeiliSearch, type Settings, type RecordAny, type IndexOptions } from "meilisearch"; import { env } from "@cvsa/env"; import { INDEX_SETTINGS } from "./config"; import { deepEqualUnordered } from "../utils"; @@ -43,15 +43,15 @@ export class SearchManager { }); const adminKey = await SearchManager.getAdminKey(masterClient); const searchKey = await SearchManager.getSearchKey(masterClient); - (this as unknown as { client: MeiliSearch }).client = new MeiliSearch({ + this.client = new MeiliSearch({ host: env.MEILI_API_URL, apiKey: searchKey, }); - (this as unknown as { adminClient: MeiliSearch }).adminClient = new MeiliSearch({ + this.adminClient = new MeiliSearch({ host: env.MEILI_API_URL, apiKey: adminKey, }); - this.syncAllSettings(); + await this.syncAllSettings(); } catch (e) { appLogger.warn("Cannot initialize SearchManager clients."); appLogger.error(Bun.inspect(e)); @@ -214,10 +214,43 @@ export class SearchManager { if (deepEqualUnordered(value, currentSettings[key as keyof Settings])) continue; // These settings do not trigger a full reindex if (["displayedAttributes", "rankingRules"].includes(key)) continue; - await index.resetSettings(); - await index.updateSettings(settings); + const resetTask = await index.resetSettings(); + await this.waitForTask(resetTask.taskUid); + const updateTask = await index.updateSettings(settings); + await this.waitForTask(updateTask.taskUid); break; } } } + + /** + * Deletes all search indexes in the MeiliSearch instance. + * This is a destructive operation that removes all indexes and their data. + * Safely no-ops if the admin client is not initialized. + */ + public async clearAllIndex() { + if (this.adminClient === undefined) { + return; + } + + const { results: indexes } = await this.listIndexes(); + for (const index of indexes) { + await this.adminClient.deleteIndexIfExists(index.uid); + appLogger.info(`Deleted search index ${index.uid}`); + } + } + + /** + * Creates a new search index with the given UID and optional configuration. + * Safely no-ops if the admin client is not initialized. + * @param uid - The unique identifier for the index + * @param options - Optional index configuration (primary key, etc.) + * @returns A promise that resolves to the created index task, or undefined if not initialized + */ + public async createIndex(uid: string, options?: IndexOptions) { + if (this.adminClient === undefined) { + return; + } + return this.adminClient.createIndex(uid, options); + } } diff --git a/packages/core/src/utils/BaseRepository.ts b/packages/core/src/utils/BaseRepository.ts new file mode 100644 index 0000000..fe9ef54 --- /dev/null +++ b/packages/core/src/utils/BaseRepository.ts @@ -0,0 +1,8 @@ +import { transformPrismaResult, type Serialized } from "@cvsa/db"; +import { traceTask } from "@cvsa/observability"; + +export abstract class BaseRepository { + protected async query(name: string, fn: () => Promise): Promise> { + return traceTask(name, async () => transformPrismaResult(await fn())); + } +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index a681d76..c03d97a 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./randomId"; export * from "./deepEqualUnordered"; +export * from "./BaseRepository"; diff --git a/packages/core/tests/integration/ArtistRepository.test.ts b/packages/core/tests/integration/ArtistRepository.test.ts new file mode 100644 index 0000000..5337078 --- /dev/null +++ b/packages/core/tests/integration/ArtistRepository.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, test } from "bun:test"; +import { + artistRepository, + type CreateArtistRequestDto, + type UpdateArtistRequestDto, +} from "@cvsa/core"; + +const repository = artistRepository; + +describe("ArtistRepository Integration Tests", () => { + describe("create", () => { + test("should create an artist with all fields", async () => { + const input: CreateArtistRequestDto = { + name: "月华P", + localizedNames: { ja: "月華P", en: "Gekka P" }, + language: "zh", + aliases: ["月光P", "Moonlight"], + description: "A famous composer in the Chinese vocal synth community", + localizedDescriptions: { + en: "A famous composer", + ja: "有名な作曲家", + }, + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("月华P"); + expect(result.localizedNames).toEqual({ ja: "月華P", en: "Gekka P" }); + expect(result.language).toBe("zh"); + expect(result.aliases).toEqual(["月光P", "Moonlight"]); + expect(result.description).toBe( + "A famous composer in the Chinese vocal synth community" + ); + expect(result.localizedDescriptions).toEqual({ + en: "A famous composer", + ja: "有名な作曲家", + }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should create an artist with minimal fields", async () => { + const input: CreateArtistRequestDto = { + name: "无名P", + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("无名P"); + expect(result.localizedNames).toBeNull(); + // language has default value "zh" in database + expect(result.language).toBe("zh"); + // aliases is String[] with default empty array + expect(result.aliases).toEqual([]); + expect(result.description).toBeNull(); + expect(result.localizedDescriptions).toBeNull(); + }); + }); + + describe("getById", () => { + test("should return artist when exists", async () => { + const created = await repository.create({ name: "赤羽P" }); + const result = await repository.getById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("赤羽P"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when artist does not exist", async () => { + const result = await repository.getById(999999); + expect(result).toBeNull(); + }); + }); + + describe("getDetailsById", () => { + test("should return artist details when exists", async () => { + const created = await repository.create({ + name: "星尘P", + description: "Composer of 星尘 series", + }); + const result = await repository.getDetailsById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("星尘P"); + expect(result?.description).toBe("Composer of 星尘 series"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when artist does not exist", async () => { + const result = await repository.getDetailsById(999999); + expect(result).toBeNull(); + }); + }); + + describe("update", () => { + test("should update all fields", async () => { + const created = await repository.create({ + name: "Old Name", + language: "zh", + aliases: ["old"], + }); + + const input: UpdateArtistRequestDto = { + name: "New Name", + language: "en", + aliases: ["new", "alias"], + description: "Updated description", + }; + + const result = await repository.update(created.id, input); + + expect(result.name).toBe("New Name"); + expect(result.language).toBe("en"); + expect(result.aliases).toEqual(["new", "alias"]); + expect(result.description).toBe("Updated description"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should update only specified fields", async () => { + const created = await repository.create({ + name: "Original Name", + language: "zh", + description: "Original description", + }); + + const result = await repository.update(created.id, { name: "Updated Name" }); + + expect(result.name).toBe("Updated Name"); + expect(result.language).toBe("zh"); + expect(result.description).toBe("Original description"); + }); + }); + + describe("softDelete", () => { + test("should soft delete an artist", async () => { + const created = await repository.create({ name: "Artist to Delete" }); + + await repository.softDelete(created.id); + + const result = await repository.getById(created.id); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/core/tests/integration/EngineRepository.test.ts b/packages/core/tests/integration/EngineRepository.test.ts new file mode 100644 index 0000000..21efcfd --- /dev/null +++ b/packages/core/tests/integration/EngineRepository.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, test } from "bun:test"; +import { + engineRepository, + type CreateEngineRequestDto, + type UpdateEngineRequestDto, +} from "@cvsa/core"; + +const repository = engineRepository; + +describe("EngineRepository Integration Tests", () => { + describe("create", () => { + test("should create an engine with all fields", async () => { + const input: CreateEngineRequestDto = { + name: "VOCALOID", + description: "The latest version of VOCALOID synthesis engine", + localizedDescriptions: { + en: "The latest version of VOCALOID", + ja: "VOCALOIDの最新版", + zh: "VOCALOID最新版", + }, + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("VOCALOID"); + expect(result.description).toBe("The latest version of VOCALOID synthesis engine"); + expect(result.localizedDescriptions).toEqual({ + en: "The latest version of VOCALOID", + ja: "VOCALOIDの最新版", + zh: "VOCALOID最新版", + }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should create an engine with minimal fields (name only)", async () => { + const input: CreateEngineRequestDto = { + name: "Synthesizer V", + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("Synthesizer V"); + expect(result.description).toBeNull(); + expect(result.localizedDescriptions).toBeNull(); + }); + }); + + describe("getById", () => { + test("should return engine when exists", async () => { + const created = await repository.create({ name: "UTAU" }); + const result = await repository.getById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("UTAU"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when engine does not exist", async () => { + const result = await repository.getById(999999); + expect(result).toBeNull(); + }); + }); + + describe("getDetailsById", () => { + test("should return engine details when exists", async () => { + const created = await repository.create({ + name: "ACE Studio", + description: "AI-powered singing synthesis engine", + }); + const result = await repository.getDetailsById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("ACE Studio"); + expect(result?.description).toBe("AI-powered singing synthesis engine"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when engine does not exist", async () => { + const result = await repository.getDetailsById(999999); + expect(result).toBeNull(); + }); + }); + + describe("update", () => { + test("should update all fields", async () => { + const created = await repository.create({ + name: "Old Engine", + description: "Old description", + }); + + const input: UpdateEngineRequestDto = { + name: "Updated Engine", + description: "Updated description", + localizedDescriptions: { + en: "Updated", + }, + }; + + const result = await repository.update(created.id, input); + + expect(result.name).toBe("Updated Engine"); + expect(result.description).toBe("Updated description"); + expect(result.localizedDescriptions).toEqual({ en: "Updated" }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should update only specified fields", async () => { + const created = await repository.create({ + name: "Original Engine", + description: "Original description", + localizedDescriptions: { zh: "原始描述" }, + }); + + const result = await repository.update(created.id, { name: "Renamed Engine" }); + + expect(result.name).toBe("Renamed Engine"); + expect(result.description).toBe("Original description"); + expect(result.localizedDescriptions).toEqual({ zh: "原始描述" }); + }); + }); + + describe("softDelete", () => { + test("should soft delete an engine", async () => { + const created = await repository.create({ name: "Engine to Delete" }); + + await repository.softDelete(created.id); + + const result = await repository.getById(created.id); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/core/tests/integration/OutboxRepository.test.ts b/packages/core/tests/integration/OutboxRepository.test.ts new file mode 100644 index 0000000..972601b --- /dev/null +++ b/packages/core/tests/integration/OutboxRepository.test.ts @@ -0,0 +1,322 @@ +import { describe, expect, test } from "bun:test"; +import { + outboxRepository, + type CreateOutboxEntryDto, + type PendingOutboxQueryDto, +} from "@cvsa/core"; + +const repository = outboxRepository; + +describe("OutboxRepository Integration Tests", () => { + describe("create", () => { + test("should create an outbox entry with all fields", async () => { + const input: CreateOutboxEntryDto = { + aggregateType: "Song", + aggregateId: 1, + eventType: "SongCreated", + payload: { title: "测试歌曲", artist: "测试歌手" }, + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.aggregateType).toBe("Song"); + expect(result.aggregateId).toBe(1); + expect(result.eventType).toBe("SongCreated"); + expect(result.payload).toEqual({ title: "测试歌曲", artist: "测试歌手" }); + expect(result.status).toBe("PENDING"); + expect(result.retryCount).toBe(0); + expect(result.lastError).toBeNull(); + expect(result.nextRetryAt).toBeNull(); + expect(result.createdAt).toBeDefined(); + expect(result.processedAt).toBeNull(); + }); + + test("should create an outbox entry with minimal fields (no payload)", async () => { + const input: CreateOutboxEntryDto = { + aggregateType: "Artist", + aggregateId: 2, + eventType: "ArtistUpdated", + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.aggregateType).toBe("Artist"); + expect(result.aggregateId).toBe(2); + expect(result.eventType).toBe("ArtistUpdated"); + expect(result.payload).toBeNull(); + expect(result.status).toBe("PENDING"); + expect(result.retryCount).toBe(0); + }); + + test("should default status to PENDING for new entries", async () => { + const result = await repository.create({ + aggregateType: "Album", + aggregateId: 3, + eventType: "AlbumCreated", + }); + + expect(result.status).toBe("PENDING"); + }); + }); + + describe("findPending", () => { + test("should return entries with PENDING status whose nextRetryAt is null or in the past", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 10, + eventType: "SongCreated", + }); + + const query: PendingOutboxQueryDto = { limit: 100 }; + const results = await repository.findPending(query); + + expect(results.length).toBeGreaterThanOrEqual(1); + const found = results.find((r) => r.id === created.id); + expect(found).toBeDefined(); + expect(found?.status).toBe("PENDING"); + }); + + test("should respect the limit parameter", async () => { + await repository.create({ + aggregateType: "Song", + aggregateId: 100, + eventType: "SongCreated", + }); + await repository.create({ + aggregateType: "Song", + aggregateId: 101, + eventType: "SongCreated", + }); + await repository.create({ + aggregateType: "Song", + aggregateId: 102, + eventType: "SongCreated", + }); + + const query: PendingOutboxQueryDto = { limit: 2 }; + const results = await repository.findPending(query); + + expect(results.length).toBeLessThanOrEqual(2); + }); + + test("should not return entries that are PROCESSED", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 200, + eventType: "SongCreated", + }); + await repository.markProcessed(created.id); + + const query: PendingOutboxQueryDto = { limit: 100 }; + const results = await repository.findPending(query); + + const found = results.find((r) => r.id === created.id); + expect(found).toBeUndefined(); + }); + + test("should not return entries that are PROCESSING", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 300, + eventType: "SongCreated", + }); + await repository.markProcessing(created.id); + + const query: PendingOutboxQueryDto = { limit: 100 }; + const results = await repository.findPending(query); + + const found = results.find((r) => r.id === created.id); + expect(found).toBeUndefined(); + }); + + test("should not return FAILED entries regardless of nextRetryAt", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 400, + eventType: "SongCreated", + }); + const pastRetry = new Date(Date.now() - 10000); + await repository.markFailed(created.id, "Test error", pastRetry); + + const query: PendingOutboxQueryDto = { limit: 100 }; + const results = await repository.findPending(query); + + const found = results.find((r) => r.id === created.id); + expect(found).toBeUndefined(); + }); + }); + + describe("markProcessing", () => { + test("should change status from PENDING to PROCESSING", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 20, + eventType: "SongCreated", + }); + + const result = await repository.markProcessing(created.id); + + expect(result).toBeDefined(); + expect(result.id).toBe(created.id); + expect(result.status).toBe("PROCESSING"); + }); + + test("should update an existing FAILED entry to PROCESSING", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 21, + eventType: "SongCreated", + }); + await repository.markFailed(created.id, "Previous error", new Date(Date.now() - 1000)); + + const result = await repository.markProcessing(created.id); + + expect(result.status).toBe("PROCESSING"); + }); + }); + + describe("markProcessed", () => { + test("should change status to PROCESSED and set processedAt", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 30, + eventType: "SongCreated", + }); + + const result = await repository.markProcessed(created.id); + + expect(result).toBeDefined(); + expect(result.id).toBe(created.id); + expect(result.status).toBe("PROCESSED"); + expect(result.processedAt).toBeDefined(); + expect(result.processedAt).not.toBeNull(); + }); + + test("should not appear in findPending after being processed", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 31, + eventType: "SongCreated", + }); + await repository.markProcessed(created.id); + + const query: PendingOutboxQueryDto = { limit: 100 }; + const results = await repository.findPending(query); + + const found = results.find((r) => r.id === created.id); + expect(found).toBeUndefined(); + }); + }); + + describe("markFailed", () => { + test("should set status to FAILED with lastError and nextRetryAt", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 40, + eventType: "SongCreated", + }); + const nextRetryAt = new Date(Date.now() + 60000); + + const result = await repository.markFailed( + created.id, + "Processing failed due to network error", + nextRetryAt + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(created.id); + expect(result.status).toBe("FAILED"); + expect(result.lastError).toBe("Processing failed due to network error"); + expect(result.nextRetryAt).toBeDefined(); + expect(result.nextRetryAt).not.toBeNull(); + }); + + test("should increment retryCount when marking as failed", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 41, + eventType: "SongCreated", + }); + + const firstFail = await repository.markFailed( + created.id, + "First failure", + new Date(Date.now() + 60000) + ); + expect(firstFail.retryCount).toBe(1); + + const secondFail = await repository.markFailed( + created.id, + "Second failure", + new Date(Date.now() + 120000) + ); + expect(secondFail.retryCount).toBe(2); + }); + + test("should retain lastError from latest failure", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 42, + eventType: "SongCreated", + }); + + await repository.markFailed(created.id, "First error", new Date(Date.now() + 60000)); + const result = await repository.markFailed( + created.id, + "Second error", + new Date(Date.now() + 120000) + ); + + expect(result.lastError).toBe("Second error"); + }); + }); + + describe("full lifecycle", () => { + test("should support PENDING -> PROCESSING -> PROCESSED lifecycle", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 50, + eventType: "SongCreated", + }); + + expect(created.status).toBe("PENDING"); + + const processing = await repository.markProcessing(created.id); + expect(processing.status).toBe("PROCESSING"); + + const processed = await repository.markProcessed(created.id); + expect(processed.status).toBe("PROCESSED"); + expect(processed.processedAt).toBeDefined(); + }); + + test("should support PENDING -> PROCESSING -> FAILED -> PROCESSING -> PROCESSED lifecycle", async () => { + const created = await repository.create({ + aggregateType: "Song", + aggregateId: 51, + eventType: "SongCreated", + }); + + const processing = await repository.markProcessing(created.id); + expect(processing.status).toBe("PROCESSING"); + + const failed = await repository.markFailed( + created.id, + "Transient error", + new Date(Date.now() - 1000) + ); + expect(failed.status).toBe("FAILED"); + expect(failed.retryCount).toBe(1); + + const retryProcessing = await repository.markProcessing(created.id); + expect(retryProcessing.status).toBe("PROCESSING"); + + const processed = await repository.markProcessed(created.id); + expect(processed.status).toBe("PROCESSED"); + expect(processed.processedAt).toBeDefined(); + }); + }); +}); diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts index 395aedc..c72c9fc 100644 --- a/packages/core/tests/integration/SongRepository.test.ts +++ b/packages/core/tests/integration/SongRepository.test.ts @@ -1,32 +1,10 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { songRepository, type CreateSongRequestDto, type UpdateSongRequestDto } from "@cvsa/core"; import { prisma } from "@cvsa/db"; const repository = songRepository; describe("SongRepository Integration Tests", () => { - afterAll(async () => { - await prisma.creation.deleteMany(); - await prisma.artistRole.deleteMany(); - await prisma.artist.deleteMany(); - await prisma.performance.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.singer.deleteMany(); - await prisma.song.deleteMany(); - await prisma.$disconnect(); - }); - - beforeAll(async () => { - await prisma.$connect(); - await prisma.creation.deleteMany(); - await prisma.artistRole.deleteMany(); - await prisma.artist.deleteMany(); - await prisma.performance.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.singer.deleteMany(); - await prisma.song.deleteMany(); - }); - describe("create", () => { test("should create a song with all fields", async () => { const input: CreateSongRequestDto = { diff --git a/packages/core/tests/integration/SongSearchService.test.ts b/packages/core/tests/integration/SongSearchService.test.ts index 1d1e721..238e88a 100644 --- a/packages/core/tests/integration/SongSearchService.test.ts +++ b/packages/core/tests/integration/SongSearchService.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import { songRepository } from "@cvsa/core"; import { prisma } from "@cvsa/db"; import { SongSearchService } from "../../src/search/catalog/song"; @@ -10,77 +10,22 @@ import { MeiliSearch } from "meilisearch"; const MEILI_HOST = env.MEILI_API_URL ?? "http://127.0.0.1:7700"; const MEILI_MASTER_KEY = env.MEILI_MASTER_KEY ?? ""; -const waitForTask = async (client: MeiliSearch, taskUid: number) => { - return client.tasks.waitForTask(taskUid, { timeout: 10000, interval: 100 }); -}; - -const cleanupIndex = async (client: MeiliSearch, indexName: string) => { - try { - const task = await client.deleteIndex(indexName); - await waitForTask(client, task.taskUid); - } catch { - // Index may not exist - } -}; - -const createIndexIfNotExists = async (client: MeiliSearch, indexName: string) => { - try { - await client.getIndex(indexName); - } catch { - const task = await client.createIndex(indexName, { primaryKey: "id" }); - await waitForTask(client, task.taskUid); - } -}; +const client = new MeiliSearch({ host: MEILI_HOST, apiKey: MEILI_MASTER_KEY }); +const searchManager = await SearchManager.create(); +const mockEmbeddingManager: EmbeddingAppApi = { + embeddings: { + post: async ({ texts }: { texts: string[] }) => { + const dimensions = 256; + const embeddings = texts.map(() => + Array.from({ length: dimensions }, () => Math.random()) + ); + return { data: { embeddings } } as never; + }, + }, +} as never; +const service = new SongSearchService(songRepository, searchManager, mockEmbeddingManager); describe("SongSearchService Integration Tests", () => { - const client = new MeiliSearch({ host: MEILI_HOST, apiKey: MEILI_MASTER_KEY }); - let searchManager: SearchManager; - let mockEmbeddingManager: EmbeddingAppApi; - - beforeAll(async () => { - await prisma.$connect(); - await cleanupIndex(client, "song_zh"); - await cleanupIndex(client, "song_en"); - await createIndexIfNotExists(client, "song_zh"); - await createIndexIfNotExists(client, "song_en"); - - searchManager = await SearchManager.create(); - - mockEmbeddingManager = { - embeddings: { - post: async ({ texts }: { texts: string[] }) => { - const dimensions = 256; - const embeddings = texts.map(() => - Array.from({ length: dimensions }, () => Math.random()) - ); - return { data: { embeddings } } as never; - }, - }, - } as never; - }); - - beforeEach(async () => { - const indexZh = client.index("song_zh"); - const indexEn = client.index("song_en"); - try { - await indexZh.deleteAllDocuments(); - await indexEn.deleteAllDocuments(); - } catch { - // Ignore - } - }); - - afterAll(async () => { - await prisma.creation.deleteMany(); - await prisma.artistRole.deleteMany(); - await prisma.artist.deleteMany(); - await prisma.performance.deleteMany(); - await prisma.lyrics.deleteMany(); - await prisma.singer.deleteMany(); - await prisma.song.deleteMany(); - await prisma.$disconnect(); - }); - describe("sync", () => { test("syncs song to search index", async () => { const singer = await prisma.singer.create({ @@ -102,11 +47,6 @@ describe("SongSearchService Integration Tests", () => { creations: [{ artistId: artist.id, roleId: artistRole.id }], }); - const service = new SongSearchService( - songRepository, - searchManager, - mockEmbeddingManager - ); await service.sync(song.id); const index = client.index("song_zh"); @@ -121,11 +61,6 @@ describe("SongSearchService Integration Tests", () => { test("deletes song from search index when song is removed", async () => { const song = await songRepository.create({ name: "待删除歌曲" }); - const service = new SongSearchService( - songRepository, - searchManager, - mockEmbeddingManager - ); await service.sync(song.id); const index = client.index("song_zh"); @@ -150,11 +85,6 @@ describe("SongSearchService Integration Tests", () => { localizedDescriptions: { en: "English description" }, }); - const service = new SongSearchService( - songRepository, - searchManager, - mockEmbeddingManager - ); await service.sync(song.id); const zhIndex = client.index("song_zh"); @@ -181,11 +111,6 @@ describe("SongSearchService Integration Tests", () => { description: "用于测试混合搜索功能", }); - const service = new SongSearchService( - songRepository, - searchManager, - mockEmbeddingManager - ); await service.sync(song.id); const result = await service.search("混合搜索", "zh"); diff --git a/packages/core/tests/unit/ArtistSearchService.test.ts b/packages/core/tests/unit/ArtistSearchService.test.ts new file mode 100644 index 0000000..c3fdeaf --- /dev/null +++ b/packages/core/tests/unit/ArtistSearchService.test.ts @@ -0,0 +1,244 @@ +import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; +import type { ArtistDetailsResponseDto } from "../../src/modules"; +import type { IRepositoryWithGetDetails } from "../../src/types/repository"; + +const mockGetLocalizedIndexesOfEntity = mock(); +const mockGetAdminIndex = mock(); +const mockGetSearchIndex = mock(); + +const mockAdminIndex = { + deleteDocument: mock(), + addDocuments: mock(), + search: mock(), +}; + +const mockSearchIndex = { + search: mock(), +}; + +const mockSearchManager = { + getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, + getAdminIndex: mockGetAdminIndex, + getSearchIndex: mockGetSearchIndex, + waitForTask: mock().mockResolvedValue(undefined), +}; + +const mockEmbeddingsPost = mock(); +const mockEmbeddingManager = { + embeddings: { + post: mockEmbeddingsPost, + }, +}; + +const { ArtistSearchService } = await import("../../src/search/catalog/artist"); + +const mockArtistDetails: ArtistDetailsResponseDto = { + id: 1, + name: "Test Artist", + language: "zh", + aliases: ["Alias1", "Alias2"], + description: "A test artist", + localizedNames: { en: "Test Artist EN", ja: "テストアーティスト" }, + localizedDescriptions: { en: "A test artist in English" }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + userId: null, +}; + +describe("ArtistSearchService", () => { + let service: InstanceType; + let mockRepository: IRepositoryWithGetDetails; + + beforeEach(() => { + mockGetLocalizedIndexesOfEntity.mockClear(); + mockGetAdminIndex.mockClear(); + mockGetSearchIndex.mockClear(); + mockAdminIndex.deleteDocument.mockClear(); + mockAdminIndex.addDocuments.mockClear(); + mockAdminIndex.search.mockClear(); + mockSearchIndex.search.mockClear(); + mockEmbeddingsPost.mockClear(); + + mockGetAdminIndex.mockResolvedValue(mockAdminIndex); + mockGetSearchIndex.mockResolvedValue(mockSearchIndex); + mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); + mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); + mockEmbeddingsPost.mockResolvedValue({ + data: { embeddings: [[0.1, 0.2, 0.3]] }, + }); + + mockRepository = { + getDetailsById: mock(), + }; + + service = new ArtistSearchService( + mockRepository, + mockSearchManager as never, + mockEmbeddingManager as never + ); + }); + + afterAll(() => { + mock.restore(); + mock.clearAllMocks(); + }); + + describe("sync", () => { + test("syncs artist to all language indexes", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockArtistDetails + ); + + await service.sync(1); + + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0] as unknown[]; + expect(callArgs[1]).toEqual({ primaryKey: "id" }); + }); + + test("deletes document from all indexes when artist not found", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); + mockGetLocalizedIndexesOfEntity.mockResolvedValue(["artist_zh", "artist_en"]); + + await service.sync(999); + + expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("artist"); + expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); + }); + + test("handles missing search manager gracefully", async () => { + const serviceWithoutManager = new ArtistSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockArtistDetails + ); + + await serviceWithoutManager.sync(1); + + expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); + }); + + test("builds document with localized content", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockArtistDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const allCalls = mockAdminIndex.addDocuments.mock.calls as unknown as [ + { id: number; name: string }[], + unknown, + ][]; + const enCall = allCalls.find((call) => call[0][0].name === "Test Artist EN"); + expect(enCall).toBeDefined(); + }); + + test("handles artist with no localized content", async () => { + const artistWithoutLocalization = { + ...mockArtistDetails, + localizedNames: {}, + localizedDescriptions: {}, + }; + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + artistWithoutLocalization + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + }); + + test("handles embedding generation failure", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockArtistDetails + ); + mockEmbeddingsPost.mockResolvedValue({ + data: null, + }); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const doc = ( + (mockAdminIndex.addDocuments.mock.calls[0] as unknown[])[0] as { + _vectors: { "potion-multilingual-128M": unknown }; + }[] + )[0]; + expect(doc._vectors).toEqual({ + "potion-multilingual-128M": null, + }); + }); + }); + + describe("search", () => { + test("performs hybrid search with embedding", async () => { + const mockSearchResult = { + hits: [{ id: 1, name: "Test Artist" }], + query: "test", + processingTimeMs: 30, + offset: 1, + limit: 1, + estimatedTotalHits: 1, + }; + mockSearchIndex.search.mockResolvedValue(mockSearchResult); + + const result = await service.search("test query", "zh"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("artist_zh"); + expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); + expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { + vector: [0.1, 0.2, 0.3], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + expect(result).toEqual(mockSearchResult); + }); + + test("uses default language when not specified", async () => { + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("artist_zh"); + }); + + test("throws when search manager not available", async () => { + const serviceWithoutManager = new ArtistSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + expect(serviceWithoutManager.search("test")).rejects.toThrow( + "Search or embedding service not available" + ); + }); + + test("handles missing embedding response gracefully", async () => { + mockEmbeddingsPost.mockResolvedValue({ data: null }); + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test", "zh"); + + expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { + vector: undefined, + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + }); + }); +}); diff --git a/packages/core/tests/unit/ArtistService.test.ts b/packages/core/tests/unit/ArtistService.test.ts new file mode 100644 index 0000000..d9f4905 --- /dev/null +++ b/packages/core/tests/unit/ArtistService.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, mock, test } from "bun:test"; +import { prisma } from "@cvsa/db"; +import { AppError } from "../../src/error/AppError"; +import { ArtistService } from "../../src/modules/catalog/artist/service"; +import type { ArtistDetailsResponseDto } from "../../src/modules/catalog/artist/dto"; +import type { IArtistRepository } from "../../src/modules/catalog/artist/repository.interface"; +import type { OutboxService } from "../../src/modules/outbox/service"; +import { createMockRepository } from "../utils"; + +( + prisma as unknown as { + $transaction: (fn: (tx: unknown) => Promise) => Promise; + } +).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); + +const mockArtistDetails: ArtistDetailsResponseDto = { + id: 1, + name: "Test Artist", + localizedNames: null, + language: "zh", + aliases: [], + description: "A test artist", + localizedDescriptions: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + userId: null, +}; + +const mockOutboxEntry = { + id: 1, + aggregateType: "artist" as const, + aggregateId: 1, + eventType: "artist.created" as const, + payload: null, + status: "PENDING" as const, + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, +}; + +describe("ArtistService", () => { + const mockRepository = createMockRepository({ + getById: async (id: number) => { + if (id === 1) { + return mockArtistDetails; + } + return null; + }, + getDetailsById: async (id: number) => { + if (id === 1) { + return mockArtistDetails; + } + return null; + }, + create: async () => mockArtistDetails, + update: async () => mockArtistDetails, + softDelete: async () => {}, + }); + + const mockOutboxService = { + createEntry: mock(async () => mockOutboxEntry), + enqueue: mock(async () => {}), + processEntry: mock(async () => {}), + recoverStaleEntries: mock(async () => {}), + }; + + const artistService = new ArtistService( + mockRepository as unknown as IArtistRepository, + mockOutboxService as unknown as OutboxService + ); + + describe("getDetails", () => { + test("returns artist details when artist exists", async () => { + const result = await artistService.getDetails(1); + + expect(result).toEqual(mockArtistDetails); + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when artist does not exist", async () => { + mockRepository.getDetailsById.mockResolvedValueOnce(null); + + expect(artistService.getDetails(999)).rejects.toThrow(AppError); + expect(artistService.getDetails(999)).rejects.toThrow("error.artist.notfound"); + expect(artistService.getDetails(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("create", () => { + const createInput = { + name: "New Artist", + language: "ja", + }; + + test("creates artist and calls outbox.createEntry and outbox.enqueue", async () => { + const result = await artistService.create(createInput); + + expect(result).toMatchObject({ + name: "Test Artist", + language: "zh", + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artist", + aggregateId: mockArtistDetails.id, + eventType: "artist.created", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + }); + + describe("update", () => { + const updateInput = { name: "Updated Artist" }; + + test("updates artist and calls outbox.createEntry and outbox.enqueue on success", async () => { + const result = await artistService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Test Artist", + language: "zh", + }); + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artist", + aggregateId: 1, + eventType: "artist.updated", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when artist does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(artistService.update(999, updateInput)).rejects.toThrow(AppError); + expect(artistService.update(999, updateInput)).rejects.toThrow("error.artist.notfound"); + expect(artistService.update(999, updateInput)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("delete", () => { + test("soft deletes artist and calls outbox.createEntry and outbox.enqueue on success", async () => { + await artistService.delete(1); + + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artist", + aggregateId: 1, + eventType: "artist.deleted", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when artist does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(artistService.delete(999)).rejects.toThrow(AppError); + expect(artistService.delete(999)).rejects.toThrow("error.artist.notfound"); + expect(artistService.delete(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); +}); diff --git a/packages/core/tests/unit/EngineService.test.ts b/packages/core/tests/unit/EngineService.test.ts new file mode 100644 index 0000000..7f47005 --- /dev/null +++ b/packages/core/tests/unit/EngineService.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test } from "bun:test"; +import { AppError } from "../../src/error/AppError"; +import { EngineService } from "../../src/modules/catalog/engine/service"; +import type { EngineDetailsResponseDto } from "../../src/modules/catalog/engine/dto"; +import type { IEngineRepository } from "../../src/modules/catalog/engine/repository.interface"; +import { createMockRepository } from "../utils"; + +const mockEngineDetails: EngineDetailsResponseDto = { + id: 1, + name: "Test Engine", + description: "A test engine", + localizedDescriptions: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +describe("EngineService", () => { + const mockRepository = createMockRepository({ + getById: async (id: number) => { + if (id === 1) { + return mockEngineDetails; + } + return null; + }, + getDetailsById: async (id: number) => { + if (id === 1) { + return mockEngineDetails; + } + return null; + }, + create: async () => mockEngineDetails, + update: async () => mockEngineDetails, + softDelete: async () => {}, + }); + + const engineService = new EngineService(mockRepository as unknown as IEngineRepository); + + describe("getDetails", () => { + test("returns engine details when engine exists", async () => { + const result = await engineService.getDetails(1); + + expect(result).toEqual(mockEngineDetails); + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when engine does not exist", async () => { + mockRepository.getDetailsById.mockResolvedValueOnce(null); + + expect(engineService.getDetails(999)).rejects.toThrow(AppError); + expect(engineService.getDetails(999)).rejects.toThrow("error.engine.notfound"); + expect(engineService.getDetails(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("create", () => { + const createInput = { + name: "New Engine", + description: "A new test engine", + }; + + test("creates engine and returns result", async () => { + const result = await engineService.create(createInput); + + expect(result).toMatchObject({ + name: "Test Engine", + description: "A test engine", + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput); + }); + }); + + describe("update", () => { + const updateInput = { name: "Updated Engine" }; + + test("updates engine on success", async () => { + const result = await engineService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Test Engine", + description: "A test engine", + }); + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); + }); + + test("throws NOT_FOUND error when engine does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(engineService.update(999, updateInput)).rejects.toThrow(AppError); + expect(engineService.update(999, updateInput)).rejects.toThrow("error.engine.notfound"); + expect(engineService.update(999, updateInput)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("delete", () => { + test("soft deletes engine on success", async () => { + await engineService.delete(1); + + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when engine does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(engineService.delete(999)).rejects.toThrow(AppError); + expect(engineService.delete(999)).rejects.toThrow("error.engine.notfound"); + expect(engineService.delete(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); +}); diff --git a/packages/core/tests/unit/OutboxProcessor.test.ts b/packages/core/tests/unit/OutboxProcessor.test.ts new file mode 100644 index 0000000..7927068 --- /dev/null +++ b/packages/core/tests/unit/OutboxProcessor.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, mock, test, beforeEach } from "bun:test"; +import { createOutboxProcessor, type OutboxProcessorDeps } from "../../src/outbox/processor"; +import type { OutboxEntryDto } from "@cvsa/core/internal"; + +function makeEntry(overrides?: Partial): OutboxEntryDto { + return { + id: 1, + aggregateType: "song", + aggregateId: 42, + eventType: "SongCreated", + payload: null, + status: "PENDING", + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, + ...overrides, + }; +} + +function makeJob(entry: OutboxEntryDto) { + return { data: entry } as never; +} + +describe("createOutboxProcessor", () => { + let capturedProcessorId: number | undefined; + let capturedCallback: ( + aggregateType: string, + aggregateId: number, + eventType: string + ) => Promise; + + const mockOutboxService = { + processEntry: mock( + async ( + id: number, + processor: ( + aggregateType: string, + aggregateId: number, + eventType: string + ) => Promise + ) => { + capturedProcessorId = id; + capturedCallback = processor; + } + ), + }; + + const mockSongSearch = { sync: mock(async () => {}) }; + const mockArtistSearch = { sync: mock(async () => {}) }; + + const deps: OutboxProcessorDeps = { + outboxService: mockOutboxService as unknown as never, + searchServices: { + song: mockSongSearch, + artist: mockArtistSearch, + }, + }; + + const processor = createOutboxProcessor(deps); + + beforeEach(() => { + mockOutboxService.processEntry.mockClear(); + mockSongSearch.sync.mockClear(); + mockArtistSearch.sync.mockClear(); + capturedProcessorId = undefined; + }); + + test("should call outboxService.processEntry with entry.id from job", async () => { + const entry = makeEntry({ id: 7 }); + const job = makeJob(entry); + + await processor(job); + + expect(capturedProcessorId).toBe(7); + expect(mockOutboxService.processEntry).toHaveBeenCalledTimes(1); + }); + + test("should dispatch to song search service for aggregateType 'song'", async () => { + const entry = makeEntry({ aggregateType: "song", aggregateId: 42 }); + const job = makeJob(entry); + + await processor(job); + + await capturedCallback("song", 42, "SongCreated"); + + expect(mockSongSearch.sync).toHaveBeenCalledWith(42); + expect(mockArtistSearch.sync).not.toHaveBeenCalled(); + }); + + test("should dispatch to artist search service for aggregateType 'artist'", async () => { + const entry = makeEntry({ aggregateType: "artist", aggregateId: 99 }); + const job = makeJob(entry); + + await processor(job); + + await capturedCallback("artist", 99, "ArtistUpdated"); + + expect(mockArtistSearch.sync).toHaveBeenCalledWith(99); + expect(mockSongSearch.sync).not.toHaveBeenCalled(); + }); + + test("should throw for unknown aggregateType", async () => { + const entry = makeEntry({ aggregateType: "unknown", aggregateId: 1 }); + const job = makeJob(entry); + + await processor(job); + + const promise = capturedCallback("unknown", 1, "UnknownEvent"); + + expect(promise).rejects.toThrow("Unknown aggregate type: unknown"); + expect(mockSongSearch.sync).not.toHaveBeenCalled(); + expect(mockArtistSearch.sync).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/tests/unit/OutboxService.test.ts b/packages/core/tests/unit/OutboxService.test.ts new file mode 100644 index 0000000..4d0d8ba --- /dev/null +++ b/packages/core/tests/unit/OutboxService.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, mock, test, beforeEach } from "bun:test"; +import { OutboxService } from "@cvsa/core/internal"; +import { Prisma } from "@cvsa/db"; +import type { CreateOutboxEntryDto, OutboxEntryDto } from "@cvsa/core"; + +const mockOutboxEntry = (overrides?: Partial): OutboxEntryDto => ({ + id: 1, + aggregateType: "Song", + aggregateId: 42, + eventType: "SongCreated", + payload: null, + status: "PENDING", + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, + ...overrides, +}); + +function prismaNotFoundError(): Prisma.PrismaClientKnownRequestError { + return new Prisma.PrismaClientKnownRequestError("Not found", { + code: "P2025", + clientVersion: "test", + }); +} + +describe("OutboxService", () => { + const mockRepository = { + create: mock(async () => mockOutboxEntry()), + findPending: mock(async () => [] as OutboxEntryDto[]), + markProcessing: mock(async (id: number) => mockOutboxEntry({ id, status: "PROCESSING" })), + markProcessed: mock(async (id: number) => + mockOutboxEntry({ id, status: "PROCESSED", processedAt: new Date().toISOString() }) + ), + markFailed: mock(async (id: number, lastError: string, nextRetryAt: Date) => + mockOutboxEntry({ + id, + status: "FAILED", + lastError, + nextRetryAt: nextRetryAt.toISOString(), + retryCount: 1, + }) + ), + }; + + const mockQueue = { + add: mock(async () => {}), + }; + + const service = new OutboxService( + mockRepository as unknown as never, + mockQueue as unknown as never + ); + + beforeEach(() => { + mockRepository.create.mockClear(); + mockRepository.findPending.mockClear(); + mockRepository.markProcessing.mockClear(); + mockRepository.markProcessed.mockClear(); + mockRepository.markFailed.mockClear(); + mockQueue.add.mockClear(); + }); + + describe("createEntry", () => { + test("should delegate to repository.create with input and tx", async () => { + const input: CreateOutboxEntryDto = { + aggregateType: "Song", + aggregateId: 42, + eventType: "SongCreated", + }; + const mockTx = {} as never; + + const result = await service.createEntry(input, mockTx); + + expect(mockRepository.create).toHaveBeenCalledWith(input, mockTx); + expect(result.id).toBe(1); + expect(result.status).toBe("PENDING"); + }); + + test("should pass payload when provided", async () => { + const input: CreateOutboxEntryDto = { + aggregateType: "Artist", + aggregateId: 10, + eventType: "ArtistUpdated", + payload: { name: "test" }, + }; + const mockTx = {} as never; + + await service.createEntry(input, mockTx); + + expect(mockRepository.create).toHaveBeenCalledWith(input, mockTx); + }); + }); + + describe("enqueue", () => { + test("should add entry to queue with correct job name and options", async () => { + const entry = mockOutboxEntry(); + + await service.enqueue(entry); + + expect(mockQueue.add).toHaveBeenCalledTimes(1); + const callArgs = mockQueue.add.mock.calls[0] as unknown[]; + expect(callArgs[0]).toBe("outbox-Song-42"); + expect(callArgs[1]).toBe(entry); + expect(callArgs[2]).toEqual({ + jobId: "outbox-1", + attempts: 5, + backoff: { type: "exponential", delay: 1000 }, + }); + }); + }); + + describe("processEntry", () => { + test("should mark processing, call processor, then mark processed on success", async () => { + const processor = mock(async () => {}); + + await service.processEntry(1, processor); + + expect(mockRepository.markProcessing).toHaveBeenCalledWith(1); + expect(processor).toHaveBeenCalledWith("Song", 42, "SongCreated"); + expect(mockRepository.markProcessed).toHaveBeenCalledWith(1); + expect(mockRepository.markFailed).not.toHaveBeenCalled(); + }); + + test("should rethrow processor error when retryCount is below max retries", async () => { + mockRepository.markProcessing.mockResolvedValueOnce( + mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 2 }) + ); + const processorError = new Error("Processing failed"); + const processor = mock(async () => { + throw processorError; + }); + + const promise = service.processEntry(1, processor); + + expect(promise).rejects.toThrow("Processing failed"); + await promise.catch(() => {}); + expect(mockRepository.markProcessed).not.toHaveBeenCalled(); + expect(mockRepository.markFailed).not.toHaveBeenCalled(); + }); + + test("should mark failed when retryCount reaches max retries", async () => { + mockRepository.markProcessing.mockResolvedValueOnce( + mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 4 }) + ); + const processor = mock(async () => { + throw new Error("Final failure"); + }); + + await service.processEntry(1, processor); + + expect(mockRepository.markFailed).toHaveBeenCalledTimes(1); + const failArgs = mockRepository.markFailed.mock.calls[0] as unknown[]; + expect(failArgs[0]).toBe(1); + expect(failArgs[1]).toBe("Final failure"); + expect(mockRepository.markProcessed).not.toHaveBeenCalled(); + }); + + test("should convert non-Error throws to string message", async () => { + mockRepository.markProcessing.mockResolvedValueOnce( + mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 4 }) + ); + const processor = mock(async () => { + throw "raw string error"; + }); + + await service.processEntry(1, processor); + + expect(mockRepository.markFailed).toHaveBeenCalledTimes(1); + const failArgs = mockRepository.markFailed.mock.calls[0] as unknown[]; + expect(failArgs[1]).toBe("raw string error"); + }); + + test("should silently skip when entry is not found (P2025)", async () => { + mockRepository.markProcessing.mockRejectedValueOnce(prismaNotFoundError()); + const processor = mock(async () => {}); + + const promise = service.processEntry(1, processor); + + expect(promise).resolves.toBeUndefined(); + await promise; + expect(processor).not.toHaveBeenCalled(); + }); + + test("should rethrow non-P2025 errors from markProcessing", async () => { + const otherError = new Error("DB connection failed"); + mockRepository.markProcessing.mockRejectedValueOnce(otherError); + const processor = mock(async () => {}); + + const promise = service.processEntry(1, processor); + + expect(promise).rejects.toThrow("DB connection failed"); + await promise.catch(() => {}); + expect(processor).not.toHaveBeenCalled(); + }); + }); + + describe("recoverStaleEntries", () => { + test("should find pending entries and enqueue each one", async () => { + const entries = [ + mockOutboxEntry({ id: 1 }), + mockOutboxEntry({ id: 2 }), + mockOutboxEntry({ id: 3 }), + ]; + mockRepository.findPending.mockResolvedValueOnce(entries); + + await service.recoverStaleEntries(50); + + expect(mockRepository.findPending).toHaveBeenCalledWith({ limit: 50 }); + expect(mockQueue.add).toHaveBeenCalledTimes(3); + const firstCall = mockQueue.add.mock.calls[0] as unknown as [ + string, + OutboxEntryDto, + unknown, + ]; + const secondCall = mockQueue.add.mock.calls[1] as unknown as [ + string, + OutboxEntryDto, + unknown, + ]; + const thirdCall = mockQueue.add.mock.calls[2] as unknown as [ + string, + OutboxEntryDto, + unknown, + ]; + expect(firstCall[0]).toBe("outbox-recover-1"); + expect(secondCall[0]).toBe("outbox-recover-2"); + expect(thirdCall[0]).toBe("outbox-recover-3"); + }); + + test("should use default limit of 100", async () => { + mockRepository.findPending.mockResolvedValueOnce([]); + + await service.recoverStaleEntries(); + + expect(mockRepository.findPending).toHaveBeenCalledWith({ limit: 100 }); + }); + + test("should handle empty pending entries", async () => { + mockRepository.findPending.mockResolvedValueOnce([]); + + await service.recoverStaleEntries(); + + expect(mockQueue.add).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/tests/unit/SearchManager.test.ts b/packages/core/tests/unit/SearchManager.test.ts index c502d47..8225f66 100644 --- a/packages/core/tests/unit/SearchManager.test.ts +++ b/packages/core/tests/unit/SearchManager.test.ts @@ -19,8 +19,14 @@ const mockGetIndexes = mock(); const mockWaitForTask = mock(); const mockGetTask = mock(); const mockIndexGetSettings = mock(); -const mockIndexResetSettings = mock(); -const mockIndexUpdateSettings = mock(); +const mockIndexResetSettings = mock(() => { + return { taskUid: 1 }; +}); +const mockIndexUpdateSettings = mock(() => { + return { taskUid: 1 }; +}); +const mockAdminDeleteIndexIfExists = mock(); +const mockAdminCreateIndex = mock(); const mockIndexInstance = { getSettings: mockIndexGetSettings, @@ -62,6 +68,8 @@ const createMockClient = (): { client: MockClient; adminClient: MockClient } => getIndexes: mockGetIndexes, getKeys: mockGetKeys, createKey: mockCreateKey, + deleteIndexIfExists: mockAdminDeleteIndexIfExists, + createIndex: mockAdminCreateIndex, tasks: { waitForTask: mockWaitForTask, getTask: mockGetTask, @@ -80,6 +88,8 @@ describe("SearchManager", () => { mockIndexUpdateSettings.mockClear(); mockWaitForTask.mockClear(); mockGetTask.mockClear(); + mockAdminDeleteIndexIfExists.mockClear(); + mockAdminCreateIndex.mockClear(); }); describe("create with custom clients", () => { @@ -382,4 +392,91 @@ describe("SearchManager", () => { expect(task).toEqual(mockTask); }); }); + + describe("clearAllIndex", () => { + test("deletes all existing indexes", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ + results: [createMockIndex("song_zh-CN"), createMockIndex("artist_en")], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.clearAllIndex(); + + expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledTimes(2); + expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledWith("song_zh-CN"); + expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledWith("artist_en"); + }); + + test("handles empty index list gracefully", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + mockGetIndexes.mockResolvedValue({ results: [] }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.clearAllIndex(); + + expect(mockAdminDeleteIndexIfExists).not.toHaveBeenCalled(); + }); + }); + + describe("createIndex", () => { + test("creates a new index via admin client", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + await manager.createIndex("album_zh"); + + expect(mockAdminCreateIndex).toHaveBeenCalledWith("album_zh", undefined); + }); + + test("passes options to createIndex when provided", async () => { + const { client, adminClient } = createMockClient(); + + mockGetKeys.mockResolvedValue({ + results: [ + { key: "admin-key", actions: ["*"], indexes: ["*"] }, + { key: "search-key", actions: ["search"], indexes: ["*"] }, + ], + }); + + const manager = await SearchManager.create( + client as unknown as MeiliSearchType, + adminClient as unknown as MeiliSearchType + ); + const options = { primaryKey: "id" }; + await manager.createIndex("album_zh", options); + + expect(mockAdminCreateIndex).toHaveBeenCalledWith("album_zh", options); + }); + }); }); diff --git a/packages/core/tests/unit/SongSearchService.test.ts b/packages/core/tests/unit/SongSearchService.test.ts index 29fb055..fd51fa1 100644 --- a/packages/core/tests/unit/SongSearchService.test.ts +++ b/packages/core/tests/unit/SongSearchService.test.ts @@ -240,7 +240,9 @@ describe("SongSearchService", () => { expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); const doc = mockAdminIndex.addDocuments.mock.calls[0][0][0]; - expect(doc._vectors).toBeUndefined(); + expect(doc._vectors).toEqual({ + "potion-multilingual-128M": null, + }); }); }); @@ -299,10 +301,7 @@ describe("SongSearchService", () => { expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { vector: undefined, - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, + hybrid: undefined, showRankingScore: true, }); }); diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts index 370a328..4a1529a 100644 --- a/packages/core/tests/unit/SongService.test.ts +++ b/packages/core/tests/unit/SongService.test.ts @@ -1,6 +1,6 @@ import { describe, expect, mock, test } from "bun:test"; import { SongService, AppError } from "@cvsa/core/internal"; -import type { SongDetailsResponseDto, ISongRepository, SongSearchService } from "@cvsa/core"; +import type { SongDetailsResponseDto, ISongRepository, OutboxService } from "@cvsa/core"; import { createMockRepository } from "../utils"; const mockSongDetails: SongDetailsResponseDto = { @@ -69,13 +69,28 @@ describe("SongService", () => { softDeleteLyric: async () => {}, }); - const mockSearchService = { - sync: mock(async (_id: number) => {}), + const mockOutboxService = { + createEntry: mock(async () => ({ + id: 1, + aggregateType: "song", + aggregateId: 1, + eventType: "song.created", + payload: null, + status: "PENDING" as const, + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, + })), + enqueue: mock(async () => {}), + processEntry: mock(async () => {}), + recoverStaleEntries: mock(async () => {}), }; const songService = new SongService( mockRepository as unknown as ISongRepository, - mockSearchService as unknown as SongSearchService + mockOutboxService as unknown as OutboxService ); describe("getDetails", () => { @@ -105,22 +120,7 @@ describe("SongService", () => { duration: 200, }; - test("creates song and calls search.sync", async () => { - const result = await songService.create(createInput); - - expect(result).toMatchObject({ - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput); - }); - - test("create song even when search.sync throws", async () => { - mockSearchService.sync.mockImplementationOnce(async () => { - throw new Error("Search service unavailable"); - }); - + test("creates song and calls outbox.createEntry", async () => { const result = await songService.create(createInput); expect(result).toMatchObject({ @@ -128,14 +128,14 @@ describe("SongService", () => { type: "ORIGINAL", duration: 180, }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput); + expect(mockRepository.create).toHaveBeenCalled(); }); }); describe("update", () => { const updateInput = { name: "Updated Song" }; - test("updates song and calls search.sync on success", async () => { + test("updates song and calls outbox.createEntry on success", async () => { const result = await songService.update(1, updateInput); expect(result).toMatchObject({ @@ -144,7 +144,7 @@ describe("SongService", () => { duration: 180, }); expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); + expect(mockRepository.update).toHaveBeenCalled(); }); test("throws NOT_FOUND error when song does not exist", async () => { @@ -157,29 +157,14 @@ describe("SongService", () => { statusCode: 404, }); }); - - test("updates song even when search.sync throws", async () => { - mockSearchService.sync.mockImplementationOnce(async () => { - throw new Error("Search service unavailable"); - }); - - const result = await songService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); - }); }); describe("delete", () => { - test("soft deletes song and calls search.sync on success", async () => { + test("soft deletes song and calls outbox.createEntry on success", async () => { await songService.delete(1); expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalled(); }); test("throws NOT_FOUND error when song does not exist", async () => { @@ -192,16 +177,6 @@ describe("SongService", () => { statusCode: 404, }); }); - - test("deletes song even when search.sync throws", async () => { - mockSearchService.sync.mockImplementationOnce(async () => { - throw new Error("Search service unavailable"); - }); - - await songService.delete(1); - - expect(mockRepository.softDelete).toHaveBeenCalledWith(1); - }); }); describe("listLyrics", () => { @@ -282,7 +257,11 @@ describe("SongService", () => { language: "zh", plainText: "Test lyrics", }); - expect(mockRepository.createLyrics).toHaveBeenCalledWith(1, createInput); + expect(mockRepository.createLyrics).toHaveBeenCalledWith( + 1, + createInput, + expect.anything() + ); }); test("throws NOT_FOUND error when song does not exist", async () => { @@ -313,7 +292,11 @@ describe("SongService", () => { const result = await songService.updateLyric(1, 1, updateInput); expect(result.plainText).toBe("Updated lyrics"); - expect(mockRepository.updateLyric).toHaveBeenCalledWith(1, updateInput); + expect(mockRepository.updateLyric).toHaveBeenCalledWith( + 1, + updateInput, + expect.anything() + ); }); test("throws NOT_FOUND error when song does not exist", async () => { @@ -350,7 +333,7 @@ describe("SongService", () => { await songService.deleteLyric(1, 1); - expect(mockRepository.softDeleteLyric).toHaveBeenCalledWith(1); + expect(mockRepository.softDeleteLyric).toHaveBeenCalledWith(1, expect.anything()); }); test("throws NOT_FOUND error when song does not exist", async () => { diff --git a/packages/db/package.json b/packages/db/package.json index b6978db..73175e5 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,6 +10,7 @@ "push": "bunx --bun prisma db push", "db:deploy": "bunx --bun prisma migrate deploy", "test:setup": "NODE_ENV=test bunx --bun prisma db push --accept-data-loss", + "test:reset": "NODE_ENV=test bunx --bun prisma db push --force-reset", "studio": "bunx --bun prisma studio", "reset": "bunx --bun prisma migrate reset" }, diff --git a/packages/db/prisma/models/meta/outbox.prisma b/packages/db/prisma/models/meta/outbox.prisma new file mode 100644 index 0000000..5416ad1 --- /dev/null +++ b/packages/db/prisma/models/meta/outbox.prisma @@ -0,0 +1,27 @@ +model Outbox { + id Int @id @default(autoincrement()) + aggregateType String @map("aggregate_type") + aggregateId Int @map("aggregate_id") + eventType String @map("event_type") + payload Json? + status OutboxStatus @default(PENDING) + retryCount Int @default(0) @map("retry_count") + lastError String? @map("last_error") + nextRetryAt DateTime? @map("next_retry_at") + createdAt DateTime @default(now()) @map("created_at") + processedAt DateTime? @map("processed_at") + + @@index([status, nextRetryAt]) + @@map("outbox") + @@schema("meta") +} + +enum OutboxStatus { + PENDING + PROCESSING + PROCESSED + FAILED + + @@map("outbox_status") + @@schema("meta") +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index de280a4..b28453c 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -2,7 +2,7 @@ import type { PrismaClient } from "./types"; export type TxClient = Omit< PrismaClient, - "$transaction" | "$connect" | "$disconnect" | "$on" | "$use" + "$transaction" | "$connect" | "$disconnect" | "$on" | "$use" | "$extends" >; export * from "./prisma"; diff --git a/packages/embedding/src/embedding.ts b/packages/embedding/src/embedding.ts index 0943f7f..7c7a744 100644 --- a/packages/embedding/src/embedding.ts +++ b/packages/embedding/src/embedding.ts @@ -10,7 +10,6 @@ const modelDir = path.join(import.meta.dir, "../../../model/"); const modelName = "alikia2x/potion-multilingual-128M-int8-strip"; const modelPath = path.join(modelDir, "./potion-strip/model.onnx"); -// TODO: Unit test for this export class EmbeddingManager { private tokenizer: PreTrainedTokenizer | null = null; private session: ort.InferenceSession | null = null; diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 26231f0..1d5e9e8 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -12,6 +12,8 @@ export const env = createEnv({ OTEL_SERVICE_VERSION: z.string().optional().default("0.0.0"), LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error"]).optional().default("info"), + + REDIS_URL: z.string().optional().default("redis://127.0.0.1:6379"), }, runtimeEnv: import.meta.env, diff --git a/turbo.json b/turbo.json index a18aab5..31beb68 100644 --- a/turbo.json +++ b/turbo.json @@ -48,6 +48,15 @@ "tsconfig.json", "$TURBO_ROOT$/packages/typescript-config/base.json" ] + }, + "@cvsa/db#test:reset": { + "cache": false + }, + "@cvsa/core#test:reset": { + "cache": false + }, + "//test:reset": { + "dependsOn": ["@cvsa/db#test:reset", "@cvsa/core#test:reset"] } } } From 97e72d270e42db402ff04063c4848156e9564fdb Mon Sep 17 00:00:00 2001 From: alikia2x Date: Wed, 29 Apr 2026 22:13:21 +0800 Subject: [PATCH 18/37] feat(singer): add CRUD endpoints for singers (#54) Implement a complete CRUD API for managing singers in the catalog. - Add GET /v2/singer/:id endpoint for retrieving singer details - Add POST /v2/singer endpoint for creating new singers (auth required) - Add PATCH /v2/singer/:id endpoint for updating singer metadata (auth required) - Add DELETE /v2/singer/:id endpoint for soft-deleting singers (auth required) - Add Zod schemas for request/response validation in packages/core - Add E2E tests covering all endpoints - Add search support for singers --- .../src/handlers/catalog/singer/create.ts | 32 ++ .../src/handlers/catalog/singer/delete.ts | 34 ++ .../src/handlers/catalog/singer/get.ts | 29 ++ .../src/handlers/catalog/singer/index.ts | 13 + .../src/handlers/catalog/singer/search.ts | 25 ++ .../src/handlers/catalog/singer/update.ts | 39 +++ apps/backend/src/handlers/index.ts | 1 + apps/backend/src/index.ts | 9 +- apps/backend/tests/singer.test.ts | 321 ++++++++++++++++++ packages/core/src/modules/catalog/index.ts | 1 + .../src/modules/catalog/singer/container.ts | 17 + .../core/src/modules/catalog/singer/dto.ts | 31 ++ .../core/src/modules/catalog/singer/index.ts | 5 + .../catalog/singer/repository.interface.ts | 23 ++ .../src/modules/catalog/singer/repository.ts | 67 ++++ .../src/modules/catalog/singer/service.ts | 89 +++++ packages/core/src/outbox/processor.ts | 3 +- packages/core/src/search/catalog/index.ts | 1 + packages/core/src/search/catalog/singer.ts | 103 ++++++ packages/core/src/search/config.ts | 10 + .../integration/SingerRepository.test.ts | 153 +++++++++ .../tests/unit/SingerSearchService.test.ts | 243 +++++++++++++ .../core/tests/unit/SingerService.test.ts | 182 ++++++++++ 23 files changed, 1429 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/handlers/catalog/singer/create.ts create mode 100644 apps/backend/src/handlers/catalog/singer/delete.ts create mode 100644 apps/backend/src/handlers/catalog/singer/get.ts create mode 100644 apps/backend/src/handlers/catalog/singer/index.ts create mode 100644 apps/backend/src/handlers/catalog/singer/search.ts create mode 100644 apps/backend/src/handlers/catalog/singer/update.ts create mode 100644 apps/backend/tests/singer.test.ts create mode 100644 packages/core/src/modules/catalog/singer/container.ts create mode 100644 packages/core/src/modules/catalog/singer/dto.ts create mode 100644 packages/core/src/modules/catalog/singer/index.ts create mode 100644 packages/core/src/modules/catalog/singer/repository.interface.ts create mode 100644 packages/core/src/modules/catalog/singer/repository.ts create mode 100644 packages/core/src/modules/catalog/singer/service.ts create mode 100644 packages/core/src/search/catalog/singer.ts create mode 100644 packages/core/tests/integration/SingerRepository.test.ts create mode 100644 packages/core/tests/unit/SingerSearchService.test.ts create mode 100644 packages/core/tests/unit/SingerService.test.ts diff --git a/apps/backend/src/handlers/catalog/singer/create.ts b/apps/backend/src/handlers/catalog/singer/create.ts new file mode 100644 index 0000000..42c6195 --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/create.ts @@ -0,0 +1,32 @@ +import { Elysia } from "elysia"; +import { + CreateSingerRequestSchema, + ErrorResponseSchema, + SingerResponseSchema, + singerService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const singerCreateHandler = new Elysia({ name: "singerCreateHandler" }) + .use(authMiddleware) + .post( + "/singer", + async ({ body, status }) => { + const singer = await traceTask("singerService.create", async () => { + return await singerService.create(body); + }); + return status(201, singer); + }, + { + body: CreateSingerRequestSchema, + detail: { + summary: "Create Singer", + description: "Create a new singer entry in the catalog. Requires authentication.", + }, + response: { + 201: SingerResponseSchema, + 401: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/singer/delete.ts b/apps/backend/src/handlers/catalog/singer/delete.ts new file mode 100644 index 0000000..4611170 --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/delete.ts @@ -0,0 +1,34 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { ErrorResponseSchema, singerService } from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const singerDeleteHandler = new Elysia({ name: "singerDeleteHandler" }) + .use(authMiddleware) + .delete( + "/singer/:id", + async ({ params, set }) => { + await traceTask("singerService.delete", async () => { + return await singerService.delete(params.id); + }); + set.status = 204; + return null; + }, + { + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Delete Singer", + description: + "Soft delete a singer from the catalog. Requires authentication. The singer record is marked as deleted but retained in the database for data integrity.", + }, + response: { + 204: z.null(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/singer/get.ts b/apps/backend/src/handlers/catalog/singer/get.ts new file mode 100644 index 0000000..d9810d2 --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/get.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import { ErrorResponseSchema, SingerDetailsResponseSchema, singerService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const singerGetHandler = new Elysia().get( + "/singer/:id", + async ({ params, status }) => { + const singer = await traceTask("singerService.getDetails", async () => { + return await singerService.getDetails(params.id); + }); + return status(200, singer); + }, + { + detail: { + summary: "Get Singer", + description: + "Retrieve detailed information about a specific singer by its ID. Includes name, description, language, and metadata.", + }, + response: { + 200: SingerDetailsResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/singer/index.ts b/apps/backend/src/handlers/catalog/singer/index.ts new file mode 100644 index 0000000..afa6e42 --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/index.ts @@ -0,0 +1,13 @@ +import { Elysia } from "elysia"; +import { singerCreateHandler } from "./create"; +import { singerUpdateHandler } from "./update"; +import { singerDeleteHandler } from "./delete"; +import { singerGetHandler } from "./get"; +import { singerSearchHandler } from "./search"; + +export const singerHandler = new Elysia({ name: "singerHandler" }) + .use(singerGetHandler) + .use(singerCreateHandler) + .use(singerUpdateHandler) + .use(singerDeleteHandler) + .use(singerSearchHandler); diff --git a/apps/backend/src/handlers/catalog/singer/search.ts b/apps/backend/src/handlers/catalog/singer/search.ts new file mode 100644 index 0000000..2485da9 --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/search.ts @@ -0,0 +1,25 @@ +import { Elysia } from "elysia"; +import { singerSearchService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +// TODO: add corresponding DTO and response schema +export const singerSearchHandler = new Elysia().get( + "/singers", + async ({ query, status }) => { + const song = await traceTask("singerSearchService.search", async () => { + return await singerSearchService.search(query.q || ""); + }); + return status(200, song); + }, + { + detail: { + summary: "Search for Singers", + description: + "Full-text search across singer names, and descriptions. Returns a list of matching signers ordered by relevance.", + }, + query: z.object({ + q: z.string().optional(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/singer/update.ts b/apps/backend/src/handlers/catalog/singer/update.ts new file mode 100644 index 0000000..519e93b --- /dev/null +++ b/apps/backend/src/handlers/catalog/singer/update.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { + UpdateSingerRequestSchema, + ErrorResponseSchema, + singerService, + SingerResponseSchema, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const singerUpdateHandler = new Elysia({ name: "singerUpdateHandler" }) + .use(authMiddleware) + .patch( + "/singer/:id", + async ({ params, body, status }) => { + const singer = await traceTask("singerService.update", async () => { + return await singerService.update(params.id, body); + }); + return status(200, singer); + }, + { + body: UpdateSingerRequestSchema, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Update Singer", + description: + "Update metadata of an existing singer identified by its ID. Requires authentication.", + }, + response: { + 200: SingerResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/index.ts b/apps/backend/src/handlers/index.ts index 768f891..4e3b6cd 100644 --- a/apps/backend/src/handlers/index.ts +++ b/apps/backend/src/handlers/index.ts @@ -2,4 +2,5 @@ export * from "./auth"; export * from "./catalog/song"; export * from "./catalog/engine"; export * from "./catalog/artist"; +export * from "./catalog/singer"; export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index d8407cd..ae61139 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -2,7 +2,13 @@ import { Elysia } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; import pkg from "../package.json"; -import { authHandler, songHandler, engineHandler, artistHandler } from "@handlers/index"; +import { + authHandler, + songHandler, + engineHandler, + artistHandler, + singerHandler, +} from "@handlers/index"; import { errorHandler } from "./errorHandler"; import { openapi } from "@elysiajs/openapi"; import { requestLoggerMiddleware } from "@/middlewares"; @@ -58,6 +64,7 @@ export const app = new Elysia({ .use(songHandler) .use(engineHandler) .use(artistHandler) + .use(singerHandler) .use(devHandler) .listen(16412); diff --git a/apps/backend/tests/singer.test.ts b/apps/backend/tests/singer.test.ts new file mode 100644 index 0000000..ace461f --- /dev/null +++ b/apps/backend/tests/singer.test.ts @@ -0,0 +1,321 @@ +import { describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@cvsa/db"; + +const api = treaty(app); + +describe("Singer E2E Tests", () => { + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: `${Math.random()}`, + password: "password123", + }); + return signup.data?.data.token; + } + + describe("GET /v2/singer/:id - Get Singer", () => { + test("should retrieve a singer", async () => { + const singer = await prisma.singer.create({ + data: { + name: "Test Singer", + description: "Test Description", + language: "zh", + }, + }); + + const { data, status } = await api.v2.singer({ id: singer.id }).get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Test Singer", + description: "Test Description", + language: "zh", + }); + }); + + test("should return 404 for nonexistent singer", async () => { + const { error, status } = await api.v2.singer({ id: 99999 }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should not retrieve soft-deleted singer", async () => { + const singer = await prisma.singer.create({ + data: { + name: "Deleted Singer", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2.singer({ id: singer.id }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("POST /v2/singer - Create Singer", () => { + test("should create a singer with authentication", async () => { + const token = await getAuthToken(); + + const payload = { + name: "New Singer", + description: "A virtual singer", + language: "zh", + }; + + const { data, status } = await api.v2.singer.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "New Singer", + description: "A virtual singer", + language: "zh", + }); + }); + + test("should return 401 without authentication", async () => { + const payload = { + name: "Test Singer", + }; + + const { error, status } = await api.v2.singer.post(payload); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should create a singer with localized names and descriptions", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Singer Name", + localizedNames: { + zh: "歌手名称", + ja: "シンガー名", + }, + description: "Description in English", + localizedDescriptions: { + zh: "中文描述", + ja: "日本語の説明", + }, + language: "zh", + }; + + const { data, status } = await api.v2.singer.post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Singer Name", + localizedNames: { + zh: "歌手名称", + ja: "シンガー名", + }, + localizedDescriptions: { + zh: "中文描述", + ja: "日本語の説明", + }, + }); + }); + + test("should create a singer with minimal fields", async () => { + const token = await getAuthToken(); + + const { data, status } = await api.v2.singer.post( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + }); + }); + }); + + describe("PATCH /v2/singer/:id - Update Singer", () => { + test("should update a singer with authentication", async () => { + const token = await getAuthToken(); + + const singer = await prisma.singer.create({ + data: { + name: "Original Name", + description: "Original Description", + language: "zh", + }, + }); + + const { data, status } = await api.v2.singer({ id: singer.id }).patch( + { name: "Updated Name", description: "Updated Description", language: "en" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.name).toBe("Updated Name"); + expect(data?.description).toBe("Updated Description"); + expect(data?.language).toBe("en"); + }); + + test("should return 401 without authentication", async () => { + const singer = await prisma.singer.create({ + data: { + name: "Test Singer", + }, + }); + + const { status } = await api.v2.singer({ id: singer.id }).patch({ + name: "Updated Name", + }); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent singer", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2.singer({ id: 999999 }).patch( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should update localized names", async () => { + const token = await getAuthToken(); + + const singer = await prisma.singer.create({ + data: { + name: "Singer", + localizedNames: { zh: "旧名" }, + }, + }); + + const { data, status } = await api.v2.singer({ id: singer.id }).patch( + { localizedNames: { zh: "新名", ja: "新しい名前" } }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.localizedNames).toEqual({ zh: "新名", ja: "新しい名前" }); + }); + }); + + describe("DELETE /v2/singer/:id - Delete Singer", () => { + test("should soft delete a singer with authentication", async () => { + const token = await getAuthToken(); + + const singer = await prisma.singer.create({ + data: { + name: "Singer to Delete", + }, + }); + + const { status } = await api.v2.singer({ id: singer.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedSinger = await prisma.singer.findUnique({ + where: { id: singer.id }, + }); + expect(deletedSinger?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const singer = await prisma.singer.create({ + data: { + name: "Test Singer", + }, + }); + + const { status } = await api.v2.singer({ id: singer.id }).delete({}, {}); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent singer", async () => { + const token = await getAuthToken(); + + const { status } = await api.v2.singer({ id: 999999 }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + }); + + test("should return 404 when trying to delete already deleted singer", async () => { + const token = await getAuthToken(); + + const singer = await prisma.singer.create({ + data: { + name: "Already Deleted", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2.singer({ id: singer.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); +}); diff --git a/packages/core/src/modules/catalog/index.ts b/packages/core/src/modules/catalog/index.ts index 6b142ba..751a0f3 100644 --- a/packages/core/src/modules/catalog/index.ts +++ b/packages/core/src/modules/catalog/index.ts @@ -1,3 +1,4 @@ export * from "./song"; export * from "./engine"; export * from "./artist"; +export * from "./singer"; diff --git a/packages/core/src/modules/catalog/singer/container.ts b/packages/core/src/modules/catalog/singer/container.ts new file mode 100644 index 0000000..daccb49 --- /dev/null +++ b/packages/core/src/modules/catalog/singer/container.ts @@ -0,0 +1,17 @@ +import { prisma } from "@cvsa/db"; +import { SingerRepository } from "./repository"; +import { SingerService } from "./service"; +import { SingerSearchService, searchManager } from "../../../search"; +import { treaty } from "@elysiajs/eden"; +import type { EmbeddingApp } from "@cvsa/embedding"; +import { outboxService } from "../../outbox/container"; + +const embeddingManager = treaty("localhost:14900"); + +export const singerRepository = new SingerRepository(prisma); +export const singerSearchService = new SingerSearchService( + singerRepository, + searchManager, + embeddingManager +); +export const singerService = new SingerService(singerRepository, outboxService); diff --git a/packages/core/src/modules/catalog/singer/dto.ts b/packages/core/src/modules/catalog/singer/dto.ts new file mode 100644 index 0000000..2ce21bf --- /dev/null +++ b/packages/core/src/modules/catalog/singer/dto.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { SingerSchema, type Serialized } from "@cvsa/db"; + +export type SingerId = number; + +export const CreateSingerRequestSchema = z.object({ + name: z.string().optional(), + avatarUrl: z.string().optional(), + language: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const UpdateSingerRequestSchema = z.object({ + name: z.string().optional(), + avatarUrl: z.string().optional(), + language: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), + description: z.string().optional(), + localizedDescriptions: z.record(z.string(), z.string()).optional(), +}); + +export const SingerResponseSchema = SingerSchema.omit({ deletedAt: true }); + +export const SingerDetailsResponseSchema = SingerResponseSchema; + +export type SingerResponseDto = Serialized>; +export type SingerDetailsResponseDto = Serialized>; +export type CreateSingerRequestDto = Serialized>; +export type UpdateSingerRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/singer/index.ts b/packages/core/src/modules/catalog/singer/index.ts new file mode 100644 index 0000000..132cf0c --- /dev/null +++ b/packages/core/src/modules/catalog/singer/index.ts @@ -0,0 +1,5 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./service"; +export * from "./repository.interface"; +export * from "./container"; diff --git a/packages/core/src/modules/catalog/singer/repository.interface.ts b/packages/core/src/modules/catalog/singer/repository.interface.ts new file mode 100644 index 0000000..a1c86ac --- /dev/null +++ b/packages/core/src/modules/catalog/singer/repository.interface.ts @@ -0,0 +1,23 @@ +import type { TxClient } from "@cvsa/db"; +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; +import type { + CreateSingerRequestDto, + SingerId, + UpdateSingerRequestDto, + SingerResponseDto, + SingerDetailsResponseDto, +} from "./dto"; + +export abstract class ISingerRepository + implements IRepositoryWithGetDetails +{ + abstract getById(id: SingerId, tx?: TxClient): Promise; + abstract getDetailsById(id: SingerId, tx?: TxClient): Promise; + abstract create(input: CreateSingerRequestDto, tx?: TxClient): Promise; + abstract update( + id: SingerId, + input: UpdateSingerRequestDto, + tx?: TxClient + ): Promise; + abstract softDelete(id: SingerId, tx?: TxClient): Promise; +} diff --git a/packages/core/src/modules/catalog/singer/repository.ts b/packages/core/src/modules/catalog/singer/repository.ts new file mode 100644 index 0000000..7f8ce59 --- /dev/null +++ b/packages/core/src/modules/catalog/singer/repository.ts @@ -0,0 +1,67 @@ +import type { PrismaClient } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; +import { BaseRepository } from "../../../utils/BaseRepository"; +import type { + CreateSingerRequestDto, + SingerId, + UpdateSingerRequestDto, + SingerDetailsResponseDto, +} from "./dto"; +import type { ISingerRepository } from "./repository.interface"; + +export class SingerRepository extends BaseRepository implements ISingerRepository { + constructor(private readonly prisma: PrismaClient) { + super(); + } + + async getById(id: SingerId, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.singer.getById", () => + client.singer.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async getDetailsById(id: SingerId, tx?: TxClient): Promise { + const client = tx ?? this.prisma; + return this.query("db.singer.getDetailsById", () => + client.singer.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async create(input: CreateSingerRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.singer.create", () => + client.singer.create({ + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async update(id: SingerId, input: UpdateSingerRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.singer.update", () => + client.singer.update({ + where: { id }, + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async softDelete(id: SingerId, tx?: TxClient) { + const client = tx ?? this.prisma; + await this.query("db.singer.softDelete", () => + client.singer.update({ + where: { id }, + data: { deletedAt: new Date() }, + }) + ); + } +} diff --git a/packages/core/src/modules/catalog/singer/service.ts b/packages/core/src/modules/catalog/singer/service.ts new file mode 100644 index 0000000..8553395 --- /dev/null +++ b/packages/core/src/modules/catalog/singer/service.ts @@ -0,0 +1,89 @@ +import type { OutboxService } from "../../outbox/service"; +import { AppError } from "../../../error/AppError"; +import type { IServiceWithGetDetails } from "../../../types/service"; +import { prisma } from "@cvsa/db"; +import type { + SingerDetailsResponseDto, + SingerId, + CreateSingerRequestDto, + UpdateSingerRequestDto, + SingerResponseDto, +} from "./dto"; +import type { ISingerRepository } from "./repository.interface"; + +export class SingerService implements IServiceWithGetDetails { + constructor( + private readonly repository: ISingerRepository, + private readonly outbox: OutboxService + ) {} + + async getDetails(id: SingerId) { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.singer.notfound", "NOT_FOUND", 404); + } + return result; + } + + async create(input: CreateSingerRequestDto): Promise { + const { singer, entry } = await prisma.$transaction(async (tx) => { + const singer = await this.repository.create(input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "singer", + aggregateId: singer.id, + eventType: "singer.created", + }, + tx + ); + return { singer, entry }; + }); + + await this.outbox.enqueue(entry); + return singer; + } + + async update(id: SingerId, input: UpdateSingerRequestDto): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.singer.notfound", "NOT_FOUND", 404); + } + + const { singer, entry } = await prisma.$transaction(async (tx) => { + const singer = await this.repository.update(id, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "singer", + aggregateId: id, + eventType: "singer.updated", + }, + tx + ); + return { singer, entry }; + }); + + await this.outbox.enqueue(entry); + return singer; + } + + async delete(id: SingerId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.singer.notfound", "NOT_FOUND", 404); + } + + const entry = await prisma.$transaction(async (tx) => { + await this.repository.softDelete(id, tx); + return await this.outbox.createEntry( + { + aggregateType: "singer", + aggregateId: id, + eventType: "singer.deleted", + }, + tx + ); + }); + + await this.outbox.enqueue(entry); + } +} diff --git a/packages/core/src/outbox/processor.ts b/packages/core/src/outbox/processor.ts index de224e7..70ad4de 100644 --- a/packages/core/src/outbox/processor.ts +++ b/packages/core/src/outbox/processor.ts @@ -1,6 +1,6 @@ import type { Job } from "bullmq"; import type { OutboxEntryDto, OutboxService } from "@cvsa/core/internal"; -import { songSearchService, artistSearchService } from "@cvsa/core/internal"; +import { songSearchService, artistSearchService, singerSearchService } from "@cvsa/core/internal"; import { outboxService } from "../modules/outbox/container"; import { appLogger } from "@cvsa/logger"; @@ -44,5 +44,6 @@ export const processOutboxEntry = createOutboxProcessor({ searchServices: { song: songSearchService, artist: artistSearchService, + singer: singerSearchService, }, }); diff --git a/packages/core/src/search/catalog/index.ts b/packages/core/src/search/catalog/index.ts index 9d9a57c..0f68be0 100644 --- a/packages/core/src/search/catalog/index.ts +++ b/packages/core/src/search/catalog/index.ts @@ -1,2 +1,3 @@ export * from "./song"; export * from "./artist"; +export * from "./singer"; diff --git a/packages/core/src/search/catalog/singer.ts b/packages/core/src/search/catalog/singer.ts new file mode 100644 index 0000000..1084c62 --- /dev/null +++ b/packages/core/src/search/catalog/singer.ts @@ -0,0 +1,103 @@ +import { ISearchService } from "../interface"; +import { unique, keys } from "remeda"; +import type { SingerDetailsResponseDto } from "../../modules"; +import { appLogger } from "@cvsa/logger"; + +export interface SingerSearchIndex { + id: number; + name?: string; + description?: string; + language?: string; + _vectors?: { + "potion-multilingual-128M": number[] | null; + }; +} + +export class SingerSearchService extends ISearchService { + private async getDocument( + singer: SingerDetailsResponseDto, + language: string + ): Promise { + const getDesc = () => { + if (language === singer.language) return singer.description; + return singer.localizedDescriptions?.[language]; + }; + const getName = () => { + if (language === singer.language) return singer.name; + return singer.localizedNames?.[language]; + }; + + const vectors = await this.embeddingManager.embeddings.post({ + texts: [ + `Name: ${getName() ?? ""} +Description: ${getDesc() ?? ""} +`, + ], + }); + return { + id: singer.id, + name: getName() ?? undefined, + description: getDesc() ?? undefined, + _vectors: vectors.data?.embeddings[0] + ? { + "potion-multilingual-128M": vectors.data?.embeddings[0], + } + : { + "potion-multilingual-128M": null, + }, + }; + } + + public async sync(id: number) { + if (!this.searchManager) { + appLogger.warn("Search service not available"); + return; + } + const singer = await this.repository.getDetailsById(id); + + if (!singer) { + const indexesToBeDeleted = + await this.searchManager.getLocalizedIndexesOfEntity("singer"); + for (const index of indexesToBeDeleted) { + const adminIndex = await this.searchManager.getAdminIndex(index); + const task = await adminIndex.deleteDocument(id); + await this.searchManager.waitForTask(task.taskUid); + } + return; + } + + const languages = unique([ + ...keys(singer.localizedNames ?? []), + ...keys(singer.localizedDescriptions ?? []), + singer.language, + ]); + for (const language of languages) { + const indexUid = `singer_${language}`; + const index = await this.searchManager.getAdminIndex(indexUid); + const document = await this.getDocument(singer, language); + const task = await index.addDocuments([document], { + primaryKey: "id", + }); + await this.searchManager.waitForTask(task.taskUid); + } + } + + public async search(query: string, language: string = "zh") { + if (!this.searchManager) { + throw new Error("Search or embedding service not available"); + } + + const index = await this.searchManager.getSearchIndex(`singer_${language}`); + const embeddingResponse = await this.embeddingManager.embeddings.post({ + texts: [query], + }); + return index.search(query, { + vector: embeddingResponse?.data?.embeddings[0], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + } +} diff --git a/packages/core/src/search/config.ts b/packages/core/src/search/config.ts index 410db0a..38937be 100644 --- a/packages/core/src/search/config.ts +++ b/packages/core/src/search/config.ts @@ -57,4 +57,14 @@ export const INDEX_SETTINGS: Record = { }, }, }, + singer: { + searchableAttributes: ["name", "description", "language"], + rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], + embedders: { + "potion-multilingual-128M": { + source: "userProvided", + dimensions: 256, + }, + }, + }, }; diff --git a/packages/core/tests/integration/SingerRepository.test.ts b/packages/core/tests/integration/SingerRepository.test.ts new file mode 100644 index 0000000..3b9a841 --- /dev/null +++ b/packages/core/tests/integration/SingerRepository.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, test } from "bun:test"; +import { + singerRepository, + type CreateSingerRequestDto, + type UpdateSingerRequestDto, +} from "@cvsa/core"; + +const repository = singerRepository; + +describe("SingerRepository Integration Tests", () => { + describe("create", () => { + test("should create a singer with all fields", async () => { + const input: CreateSingerRequestDto = { + name: "洛天依", + avatarUrl: "https://example.com/luotianyi.jpg", + localizedNames: { ja: "洛天依", en: "Luo Tianyi" }, + language: "zh", + description: "A famous Chinese virtual singer developed by Shanghai Henian", + localizedDescriptions: { + en: "A famous Chinese virtual singer", + ja: "有名な中国のバーチャルシンガー", + }, + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("洛天依"); + expect(result.avatarUrl).toBe("https://example.com/luotianyi.jpg"); + expect(result.localizedNames).toEqual({ ja: "洛天依", en: "Luo Tianyi" }); + expect(result.language).toBe("zh"); + expect(result.description).toBe( + "A famous Chinese virtual singer developed by Shanghai Henian" + ); + expect(result.localizedDescriptions).toEqual({ + en: "A famous Chinese virtual singer", + ja: "有名な中国のバーチャルシンガー", + }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should create a singer with minimal fields", async () => { + const input: CreateSingerRequestDto = {}; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBeNull(); + expect(result.language).toBe("zh"); + expect(result.description).toBeNull(); + expect(result.localizedNames).toBeNull(); + expect(result.localizedDescriptions).toBeNull(); + }); + }); + + describe("getById", () => { + test("should return singer when exists", async () => { + const created = await repository.create({ name: "乐正绫" }); + const result = await repository.getById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("乐正绫"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when singer does not exist", async () => { + const result = await repository.getById(999999); + expect(result).toBeNull(); + }); + }); + + describe("getDetailsById", () => { + test("should return singer details when exists", async () => { + const created = await repository.create({ + name: "言和", + description: "Voicebank of YANHE", + }); + const result = await repository.getDetailsById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("言和"); + expect(result?.description).toBe("Voicebank of YANHE"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when singer does not exist", async () => { + const result = await repository.getDetailsById(999999); + expect(result).toBeNull(); + }); + }); + + describe("update", () => { + test("should update all fields", async () => { + const created = await repository.create({ + name: "Old Name", + language: "zh", + description: "Old description", + }); + + const input: UpdateSingerRequestDto = { + name: "New Name", + avatarUrl: "https://example.com/new.jpg", + language: "ja", + description: "Updated description", + localizedNames: { en: "New Name EN" }, + localizedDescriptions: { en: "Updated description EN" }, + }; + + const result = await repository.update(created.id, input); + + expect(result.name).toBe("New Name"); + expect(result.avatarUrl).toBe("https://example.com/new.jpg"); + expect(result.language).toBe("ja"); + expect(result.description).toBe("Updated description"); + expect(result.localizedNames).toEqual({ en: "New Name EN" }); + expect(result.localizedDescriptions).toEqual({ en: "Updated description EN" }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should update only specified fields", async () => { + const created = await repository.create({ + name: "Original Name", + language: "zh", + description: "Original description", + }); + + const result = await repository.update(created.id, { name: "Updated Name" }); + + expect(result.name).toBe("Updated Name"); + expect(result.language).toBe("zh"); + expect(result.description).toBe("Original description"); + }); + }); + + describe("softDelete", () => { + test("should soft delete a singer", async () => { + const created = await repository.create({ name: "Singer to Delete" }); + + await repository.softDelete(created.id); + + const result = await repository.getById(created.id); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/core/tests/unit/SingerSearchService.test.ts b/packages/core/tests/unit/SingerSearchService.test.ts new file mode 100644 index 0000000..54a81a5 --- /dev/null +++ b/packages/core/tests/unit/SingerSearchService.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; +import type { SingerDetailsResponseDto } from "../../src/modules"; +import type { IRepositoryWithGetDetails } from "../../src/types/repository"; + +const mockGetLocalizedIndexesOfEntity = mock(); +const mockGetAdminIndex = mock(); +const mockGetSearchIndex = mock(); + +const mockAdminIndex = { + deleteDocument: mock(), + addDocuments: mock(), + search: mock(), +}; + +const mockSearchIndex = { + search: mock(), +}; + +const mockSearchManager = { + getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, + getAdminIndex: mockGetAdminIndex, + getSearchIndex: mockGetSearchIndex, + waitForTask: mock().mockResolvedValue(undefined), +}; + +const mockEmbeddingsPost = mock(); +const mockEmbeddingManager = { + embeddings: { + post: mockEmbeddingsPost, + }, +}; + +const { SingerSearchService } = await import("../../src/search/catalog/singer"); + +const mockSingerDetails: SingerDetailsResponseDto = { + id: 1, + name: "Test Singer", + language: "zh", + avatarUrl: null, + description: "A test singer", + localizedNames: { en: "Test Singer EN", ja: "テストシンガー" }, + localizedDescriptions: { en: "A test singer in English" }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +describe("SingerSearchService", () => { + let service: InstanceType; + let mockRepository: IRepositoryWithGetDetails; + + beforeEach(() => { + mockGetLocalizedIndexesOfEntity.mockClear(); + mockGetAdminIndex.mockClear(); + mockGetSearchIndex.mockClear(); + mockAdminIndex.deleteDocument.mockClear(); + mockAdminIndex.addDocuments.mockClear(); + mockAdminIndex.search.mockClear(); + mockSearchIndex.search.mockClear(); + mockEmbeddingsPost.mockClear(); + + mockGetAdminIndex.mockResolvedValue(mockAdminIndex); + mockGetSearchIndex.mockResolvedValue(mockSearchIndex); + mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); + mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); + mockEmbeddingsPost.mockResolvedValue({ + data: { embeddings: [[0.1, 0.2, 0.3]] }, + }); + + mockRepository = { + getDetailsById: mock(), + }; + + service = new SingerSearchService( + mockRepository, + mockSearchManager as never, + mockEmbeddingManager as never + ); + }); + + afterAll(() => { + mock.restore(); + mock.clearAllMocks(); + }); + + describe("sync", () => { + test("syncs singer to all language indexes", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSingerDetails + ); + + await service.sync(1); + + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const callArgs = mockAdminIndex.addDocuments.mock.calls[0] as unknown[]; + expect(callArgs[1]).toEqual({ primaryKey: "id" }); + }); + + test("deletes document from all indexes when singer not found", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); + mockGetLocalizedIndexesOfEntity.mockResolvedValue(["singer_zh", "singer_en"]); + + await service.sync(999); + + expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("singer"); + expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); + expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); + }); + + test("handles missing search manager gracefully", async () => { + const serviceWithoutManager = new SingerSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSingerDetails + ); + + await serviceWithoutManager.sync(1); + + expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); + }); + + test("builds document with localized content", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSingerDetails + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const allCalls = mockAdminIndex.addDocuments.mock.calls as unknown as [ + { id: number; name: string }[], + unknown, + ][]; + const enCall = allCalls.find((call) => call[0][0].name === "Test Singer EN"); + expect(enCall).toBeDefined(); + }); + + test("handles singer with no localized content", async () => { + const singerWithoutLocalization = { + ...mockSingerDetails, + localizedNames: {}, + localizedDescriptions: {}, + }; + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + singerWithoutLocalization + ); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + }); + + test("handles embedding generation failure", async () => { + (mockRepository.getDetailsById as ReturnType).mockResolvedValue( + mockSingerDetails + ); + mockEmbeddingsPost.mockResolvedValue({ + data: null, + }); + + await service.sync(1); + + expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); + const doc = ( + (mockAdminIndex.addDocuments.mock.calls[0] as unknown[])[0] as { + _vectors: { "potion-multilingual-128M": unknown }; + }[] + )[0]; + expect(doc._vectors).toEqual({ + "potion-multilingual-128M": null, + }); + }); + }); + + describe("search", () => { + test("performs hybrid search with embedding", async () => { + const mockSearchResult = { + hits: [{ id: 1, name: "Test Singer" }], + query: "test", + processingTimeMs: 30, + offset: 1, + limit: 1, + estimatedTotalHits: 1, + }; + mockSearchIndex.search.mockResolvedValue(mockSearchResult); + + const result = await service.search("test query", "zh"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("singer_zh"); + expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); + expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { + vector: [0.1, 0.2, 0.3], + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + expect(result).toEqual(mockSearchResult); + }); + + test("uses default language when not specified", async () => { + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test"); + + expect(mockGetSearchIndex).toHaveBeenCalledWith("singer_zh"); + }); + + test("throws when search manager not available", async () => { + const serviceWithoutManager = new SingerSearchService( + mockRepository, + undefined as never, + mockEmbeddingManager as never + ); + + expect(serviceWithoutManager.search("test")).rejects.toThrow( + "Search or embedding service not available" + ); + }); + + test("handles missing embedding response gracefully", async () => { + mockEmbeddingsPost.mockResolvedValue({ data: null }); + mockSearchIndex.search.mockResolvedValue({ hits: [] }); + + await service.search("test", "zh"); + + expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { + vector: undefined, + hybrid: { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + }, + showRankingScore: true, + }); + }); + }); +}); diff --git a/packages/core/tests/unit/SingerService.test.ts b/packages/core/tests/unit/SingerService.test.ts new file mode 100644 index 0000000..437e267 --- /dev/null +++ b/packages/core/tests/unit/SingerService.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, mock, test } from "bun:test"; +import { prisma } from "@cvsa/db"; +import { AppError } from "../../src/error/AppError"; +import { SingerService } from "../../src/modules/catalog/singer/service"; +import type { SingerDetailsResponseDto } from "../../src/modules/catalog/singer/dto"; +import type { ISingerRepository } from "../../src/modules/catalog/singer/repository.interface"; +import type { OutboxService } from "../../src/modules/outbox/service"; +import { createMockRepository } from "../utils"; + +( + prisma as unknown as { + $transaction: (fn: (tx: unknown) => Promise) => Promise; + } +).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); + +const mockSingerDetails: SingerDetailsResponseDto = { + id: 1, + name: "Test Singer", + avatarUrl: null, + language: "zh", + localizedNames: null, + description: "A test singer", + localizedDescriptions: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const mockOutboxEntry = { + id: 1, + aggregateType: "singer" as const, + aggregateId: 1, + eventType: "singer.created" as const, + payload: null, + status: "PENDING" as const, + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, +}; + +describe("SingerService", () => { + const mockRepository = createMockRepository({ + getById: async (id: number) => { + if (id === 1) { + return mockSingerDetails; + } + return null; + }, + getDetailsById: async (id: number) => { + if (id === 1) { + return mockSingerDetails; + } + return null; + }, + create: async () => mockSingerDetails, + update: async () => mockSingerDetails, + softDelete: async () => {}, + }); + + const mockOutboxService = { + createEntry: mock(async () => mockOutboxEntry), + enqueue: mock(async () => {}), + processEntry: mock(async () => {}), + recoverStaleEntries: mock(async () => {}), + }; + + const singerService = new SingerService( + mockRepository as unknown as ISingerRepository, + mockOutboxService as unknown as OutboxService + ); + + describe("getDetails", () => { + test("returns singer details when singer exists", async () => { + const result = await singerService.getDetails(1); + + expect(result).toEqual(mockSingerDetails); + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when singer does not exist", async () => { + mockRepository.getDetailsById.mockResolvedValueOnce(null); + + expect(singerService.getDetails(999)).rejects.toThrow(AppError); + expect(singerService.getDetails(999)).rejects.toThrow("error.singer.notfound"); + expect(singerService.getDetails(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("create", () => { + const createInput = { + name: "New Singer", + language: "ja", + }; + + test("creates singer and calls outbox.createEntry and outbox.enqueue", async () => { + const result = await singerService.create(createInput); + + expect(result).toMatchObject({ + name: "Test Singer", + language: "zh", + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "singer", + aggregateId: mockSingerDetails.id, + eventType: "singer.created", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + }); + + describe("update", () => { + const updateInput = { name: "Updated Singer" }; + + test("updates singer and calls outbox.createEntry and outbox.enqueue on success", async () => { + const result = await singerService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Test Singer", + language: "zh", + }); + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "singer", + aggregateId: 1, + eventType: "singer.updated", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when singer does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(singerService.update(999, updateInput)).rejects.toThrow(AppError); + expect(singerService.update(999, updateInput)).rejects.toThrow("error.singer.notfound"); + expect(singerService.update(999, updateInput)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("delete", () => { + test("soft deletes singer and calls outbox.createEntry and outbox.enqueue on success", async () => { + await singerService.delete(1); + + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "singer", + aggregateId: 1, + eventType: "singer.deleted", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when singer does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(singerService.delete(999)).rejects.toThrow(AppError); + expect(singerService.delete(999)).rejects.toThrow("error.singer.notfound"); + expect(singerService.delete(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); +}); From 676719123abf9b8b072c65c62a4319026af13607 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 17:56:39 +0800 Subject: [PATCH 19/37] docs(guide): update developer setup guide with redis and env notes add redis and meilisearch env config examples, update required services list, add notes about remote docker compose and env customization --- apps/docs/content/internal/developers/guide.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/internal/developers/guide.md b/apps/docs/content/internal/developers/guide.md index 2c14920..1a7d4be 100644 --- a/apps/docs/content/internal/developers/guide.md +++ b/apps/docs/content/internal/developers/guide.md @@ -43,23 +43,32 @@ cp apps/backend/.env.example apps/backend/.env cp apps/backend/.env.example apps/backend/.env.test ``` -为了避免开发和测试环境冲突,你需要将所有 `.env.test` 中的 `DATABASE_URL` 修改为用于测试的数据库。例如: +为了避免开发和测试环境冲突,你需要将所有 `.env.test` 中的 `DATABASE_URL`、`MEILI_API_URL` 和 `REDIS_URL` 修改为用于测试的地址。例如: ```bash # 在 .env 中 DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa +MEILI_API_URL=http://localhost:7700 +REDIS_URL=redis://localhost:6379 # 在 .env.test 中 DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa_test +MEILI_API_URL=http://localhost:7700 +REDIS_URL=redis://localhost:6379 ``` +如果`.env`和`.env.test`中缺少配置项或参数,可以手动添加,但不要修改`.ts`文件中的默认值。 +`MEILI_MASTER_KEY` 通常不需要更改,保持默认值即可。`cvsa`和`password`需要和`.env.docker`文件中的值一致。 + ## 启动外部服务 -你需要准备一个 PostgreSQL 18 以及 MeiliSearch 1.40 实例来辅助开发和调试。推荐使用 Docker,因为它易于管理和隔离。 +你需要准备一个 PostgreSQL 18 、Redis 7 以及 MeiliSearch 1.40 实例来辅助开发和调试。推荐使用 Docker,因为它易于管理和隔离。 确保你安装了 [Docker Compose](https://docs.docker.com/compose/install/),之后便可以在根路径下通过 compose 拉起这两个容器。 ```bash docker compose up -d ``` +如果 Docker 服务不在本机,可能需要使用`docker-compose-remote.yml`来启动服务。需要将`.env.docker`一起复制到远端配置目录。 + ## 初始化项目 在开始开发前,需要安装依赖并运行初始化脚本。 From 1310a6f7a468bca9366ea52680b50b0a24ecded3 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 18:56:38 +0800 Subject: [PATCH 20/37] docs: add og metadata and fix punctuation in docs Standardized OG title metadata by adding the 'Project CVSA' prefix across all docs, fixed trailing punctuation on select pages, and improved SEO presentation and formatting consistency. --- .../internal/developers/architecture.md | 1 + .../internal/developers/crawler/meta.json | 3 +- .../internal/developers/crawler/overview.md | 1 + .../docs/content/internal/developers/guide.md | 1 + .../content/internal/developers/meta.json | 3 +- .../internal/developers/style-guide.md | 1 + .../content/internal/developers/workflow.md | 1 + apps/docs/content/internal/index.md | 3 +- .../internal/legacy/architecture-overview.md | 3 +- .../internal/legacy/crawling-lifecycle.md | 3 +- .../legacy/database-schema-reference.md | 3 +- .../legacy/message-queue-reference.md | 3 +- apps/docs/content/internal/legacy/meta.json | 1 + .../legacy/network-proxy-architecture.md | 3 +- .../internal/legacy/snapshot-execution.md | 3 +- .../internal/legacy/snapshot-scheduling.md | 3 +- .../legacy/song-collection-pipeline.md | 3 +- .../legacy/video-discovery-pipeline.md | 3 +- apps/docs/content/internal/meta.json | 1 + apps/docs/content/not-found.en.md | 1 + apps/docs/content/not-found.md | 4 +- bun.lock | 1464 ++++++++--------- 22 files changed, 767 insertions(+), 745 deletions(-) diff --git a/apps/docs/content/internal/developers/architecture.md b/apps/docs/content/internal/developers/architecture.md index dcc46ba..fb96765 100644 --- a/apps/docs/content/internal/developers/architecture.md +++ b/apps/docs/content/internal/developers/architecture.md @@ -1,6 +1,7 @@ --- title: 项目架构 description: 了解档案馆的项目组织架构。 +og:title: Project CVSA 项目架构 --- ## 目录结构 diff --git a/apps/docs/content/internal/developers/crawler/meta.json b/apps/docs/content/internal/developers/crawler/meta.json index fe69897..3f0762f 100644 --- a/apps/docs/content/internal/developers/crawler/meta.json +++ b/apps/docs/content/internal/developers/crawler/meta.json @@ -1,3 +1,4 @@ { - "title": "爬虫架构" + "title": "爬虫架构", + "og:title": "Project CVSA 爬虫架构" } diff --git a/apps/docs/content/internal/developers/crawler/overview.md b/apps/docs/content/internal/developers/crawler/overview.md index 1c3eb35..b1b9f22 100644 --- a/apps/docs/content/internal/developers/crawler/overview.md +++ b/apps/docs/content/internal/developers/crawler/overview.md @@ -1,6 +1,7 @@ --- title: 新爬虫架构概览 description: CVSA 2.0 爬虫系统的总体设计、核心设计目标与节点角色定义 +og:title: 新爬虫架构概览 --- ## 设计目标 diff --git a/apps/docs/content/internal/developers/guide.md b/apps/docs/content/internal/developers/guide.md index 1a7d4be..fc1d849 100644 --- a/apps/docs/content/internal/developers/guide.md +++ b/apps/docs/content/internal/developers/guide.md @@ -1,6 +1,7 @@ --- title: 开发者指南 description: 了解如何参与档案馆的开发工作。 +og:title: Project CVSA 开发开发者指南 --- ## 准备环境 diff --git a/apps/docs/content/internal/developers/meta.json b/apps/docs/content/internal/developers/meta.json index 7e372a7..51461ae 100644 --- a/apps/docs/content/internal/developers/meta.json +++ b/apps/docs/content/internal/developers/meta.json @@ -1,3 +1,4 @@ { - "title": "开发文档" + "title": "开发文档", + "og:title": "Project CVSA 开发文档" } diff --git a/apps/docs/content/internal/developers/style-guide.md b/apps/docs/content/internal/developers/style-guide.md index ca2b946..3d90f9a 100644 --- a/apps/docs/content/internal/developers/style-guide.md +++ b/apps/docs/content/internal/developers/style-guide.md @@ -1,6 +1,7 @@ --- title: 代码风格指南 description: 档案馆项目的代码风格、架构决策及类型安全规范。 +og:title: Project CVSA 代码风格指南 --- ## TypeScript 代码规范 diff --git a/apps/docs/content/internal/developers/workflow.md b/apps/docs/content/internal/developers/workflow.md index 61ed64a..1a72f89 100644 --- a/apps/docs/content/internal/developers/workflow.md +++ b/apps/docs/content/internal/developers/workflow.md @@ -1,6 +1,7 @@ --- title: 流程规范 description: 了解档案馆的开发工作流和相关规范。 +og:title: Project CVSA 流程规范 --- ## 分支命名 diff --git a/apps/docs/content/internal/index.md b/apps/docs/content/internal/index.md index b80a1d9..27db6fe 100644 --- a/apps/docs/content/internal/index.md +++ b/apps/docs/content/internal/index.md @@ -1,6 +1,7 @@ --- title: 成员指南 -description: 档案馆成员的内部协作指南 +description: 档案馆成员的内部协作指南。 +og:title: Project CVSA 内部成员协作指南 --- 欢迎加入中 V 档案馆!本指南旨在帮助新成员快速了解项目运作模式、协作规范及工具使用方法。 diff --git a/apps/docs/content/internal/legacy/architecture-overview.md b/apps/docs/content/internal/legacy/architecture-overview.md index 0d47808..fe732fd 100644 --- a/apps/docs/content/internal/legacy/architecture-overview.md +++ b/apps/docs/content/internal/legacy/architecture-overview.md @@ -1,6 +1,7 @@ --- title: 系统架构概览 -description: 爬虫系统的整体架构、核心组件与数据流向 +description: 爬虫系统的整体架构、核心组件与数据流向。 +og:title: Project CVSA 系统架构概览 --- CVSA 的爬虫系统是一个基于 BullMQ 消息队列的数据采集系统,用于持续追踪 Bilibili 视频的播放数据。 diff --git a/apps/docs/content/internal/legacy/crawling-lifecycle.md b/apps/docs/content/internal/legacy/crawling-lifecycle.md index 02a2411..7137818 100644 --- a/apps/docs/content/internal/legacy/crawling-lifecycle.md +++ b/apps/docs/content/internal/legacy/crawling-lifecycle.md @@ -1,6 +1,7 @@ --- title: 一次完整爬取的生命周期 -description: 从视频发现到成就追踪的全流程示例,展示爬虫系统如何处理一个新视频的完整生命周期 +description: 从视频发现到成就追踪的全流程示例,展示爬虫系统如何处理一个新视频的完整生命周期。 +og:title: 一次完整爬取的生命周期 --- 以一个**新发现的B站视频**(aid=12345)为例,展示从发现到持续追踪的完整流程。 diff --git a/apps/docs/content/internal/legacy/database-schema-reference.md b/apps/docs/content/internal/legacy/database-schema-reference.md index 93446e5..46e6841 100644 --- a/apps/docs/content/internal/legacy/database-schema-reference.md +++ b/apps/docs/content/internal/legacy/database-schema-reference.md @@ -1,6 +1,7 @@ --- title: 核心数据表 -description: 爬虫系统关键数据表的结构与用途 +description: 爬虫系统关键数据表的结构与用途。 +og:title: Project CVSA 核心数据表 --- ## snapshot_schedule diff --git a/apps/docs/content/internal/legacy/message-queue-reference.md b/apps/docs/content/internal/legacy/message-queue-reference.md index c7e8a1b..169574e 100644 --- a/apps/docs/content/internal/legacy/message-queue-reference.md +++ b/apps/docs/content/internal/legacy/message-queue-reference.md @@ -1,6 +1,7 @@ --- title: 消息队列与任务定义 -description: BullMQ队列配置、定时任务与Job数据结构 +description: BullMQ队列配置、定时任务与Job数据结构。 +og:title: Project CVSA 消息队列与任务定义 --- 爬虫系统使用BullMQ作为消息队列,Redis作为底层存储。 diff --git a/apps/docs/content/internal/legacy/meta.json b/apps/docs/content/internal/legacy/meta.json index f0a15e8..7d5dce6 100644 --- a/apps/docs/content/internal/legacy/meta.json +++ b/apps/docs/content/internal/legacy/meta.json @@ -1,5 +1,6 @@ { "title": "旧版爬虫文档", + "og:title": "Project CVSA 旧版爬虫文档", "pages": [ "architecture-overview", "crawling-lifecycle", diff --git a/apps/docs/content/internal/legacy/network-proxy-architecture.md b/apps/docs/content/internal/legacy/network-proxy-architecture.md index 51ee250..6fb5bea 100644 --- a/apps/docs/content/internal/legacy/network-proxy-architecture.md +++ b/apps/docs/content/internal/legacy/network-proxy-architecture.md @@ -1,6 +1,7 @@ --- title: 网络代理架构 -description: 代理类型、请求路由与限速配置 +description: 代理类型、请求路由与限速配置。 +og:title: Project CVSA 网络代理架构 --- 网络层负责提供一个网络请求的抽象层,用于将API请求转发到不同的代理后端,并提供限速和流控。 diff --git a/apps/docs/content/internal/legacy/snapshot-execution.md b/apps/docs/content/internal/legacy/snapshot-execution.md index a753d29..1e5dddb 100644 --- a/apps/docs/content/internal/legacy/snapshot-execution.md +++ b/apps/docs/content/internal/legacy/snapshot-execution.md @@ -1,6 +1,7 @@ --- title: 快照执行与错误处理 -description: 快照任务的执行流程、批量处理与错误恢复机制 +description: 快照任务的执行流程、批量处理与错误恢复机制。 +og:title: Project CVSA 快照执行与错误处理 --- 快照执行系统负责实际从B站获取视频数据并写入数据库。 diff --git a/apps/docs/content/internal/legacy/snapshot-scheduling.md b/apps/docs/content/internal/legacy/snapshot-scheduling.md index f302441..5b5efd1 100644 --- a/apps/docs/content/internal/legacy/snapshot-scheduling.md +++ b/apps/docs/content/internal/legacy/snapshot-scheduling.md @@ -1,6 +1,7 @@ --- title: 快照调度系统 -description: 快照调度的三种类型、时间计算策略与流量控制机制 +description: 快照调度的三种类型、时间计算策略与流量控制机制。 +og:title: Project CVSA 快照调度系统 --- 快照调度系统负责决定何时对哪些视频执行数据采集,核心数据存储于 `snapshot_schedule` 表。 diff --git a/apps/docs/content/internal/legacy/song-collection-pipeline.md b/apps/docs/content/internal/legacy/song-collection-pipeline.md index 20e859f..e66aada 100644 --- a/apps/docs/content/internal/legacy/song-collection-pipeline.md +++ b/apps/docs/content/internal/legacy/song-collection-pipeline.md @@ -1,6 +1,7 @@ --- title: 歌曲收集流程 -description: 将ML分类为歌曲的视频收集到songs表并触发追踪 +description: 将ML分类为歌曲的视频收集到songs表并触发追踪。 +og:title: Project CVSA 歌曲收集流程 --- 歌曲收集流程负责将ML分类为歌曲的视频正式纳入歌曲库,并开始持续的数据追踪。 diff --git a/apps/docs/content/internal/legacy/video-discovery-pipeline.md b/apps/docs/content/internal/legacy/video-discovery-pipeline.md index e85788f..323eb7f 100644 --- a/apps/docs/content/internal/legacy/video-discovery-pipeline.md +++ b/apps/docs/content/internal/legacy/video-discovery-pipeline.md @@ -1,6 +1,7 @@ --- title: 视频发现与元数据采集 -description: 从B站发现新视频、采集元数据并触发分类的流程 +description: 从B站发现新视频、采集元数据并触发分类的流程。 +og:title: Project CVSA 视频发现与元数据采集 --- 视频发现流程负责从Bilibili获取最新视频,采集元数据,并触发后续的分类和追踪。 diff --git a/apps/docs/content/internal/meta.json b/apps/docs/content/internal/meta.json index a4af6b5..385ee48 100644 --- a/apps/docs/content/internal/meta.json +++ b/apps/docs/content/internal/meta.json @@ -1,5 +1,6 @@ { "title": "内部文档", + "og:title": "Project CVSA 内部文档", "description": "供档案馆团队成员使用的内部文档", "root": true } diff --git a/apps/docs/content/not-found.en.md b/apps/docs/content/not-found.en.md index 1be3ce8..0092488 100644 --- a/apps/docs/content/not-found.en.md +++ b/apps/docs/content/not-found.en.md @@ -1,5 +1,6 @@ --- title: Oops! We can’t find the page you’re looking for. +og:title: Oops! Page Not Found --- It seems the link is broken, or the page has been moved to a new location. diff --git a/apps/docs/content/not-found.md b/apps/docs/content/not-found.md index 7bfb5d3..15f4490 100644 --- a/apps/docs/content/not-found.md +++ b/apps/docs/content/not-found.md @@ -1,5 +1,7 @@ --- -title: 抱歉,您访问的页面不存在 +title: 抱歉,您访问的页面不存在。 +og:title: 页面不存在 +og:image: --- 看起来我们要找的内容暂时“走丢”了,或者该链接已经失效。 diff --git a/bun.lock b/bun.lock index 474fcc4..822d287 100644 --- a/bun.lock +++ b/bun.lock @@ -83,265 +83,265 @@ }, }, "packages": { - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], + "@biomejs/biome": ["@biomejs/biome@2.4.6", "https://registry.npmmirror.com/@biomejs/biome/-/biome-2.4.6.tgz", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.6.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.6.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.6.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "https://registry.npmmirror.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.6.tgz", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="], - "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "https://registry.npmmirror.com/@borewit/text-codec/-/text-codec-0.2.1.tgz", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], - "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], - "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "https://registry.npmmirror.com/@chevrotain/gast/-/gast-10.5.0.tgz", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], - "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], + "@chevrotain/types": ["@chevrotain/types@10.5.0", "https://registry.npmmirror.com/@chevrotain/types/-/types-10.5.0.tgz", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], - "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "https://registry.npmmirror.com/@chevrotain/utils/-/utils-10.5.0.tgz", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], - "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "https://registry.npmmirror.com/@electric-sql/pglite/-/pglite-0.3.15.tgz", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], - "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="], + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "https://registry.npmmirror.com/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="], - "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "https://registry.npmmirror.com/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], - "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "https://registry.npmmirror.com/@elysiajs/eden/-/eden-1.4.8.tgz", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="], - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], + "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="], + "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "https://registry.npmmirror.com/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="], - "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], + "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "https://registry.npmmirror.com/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], - "@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.3", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ=="], + "@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.3", "https://registry.npmmirror.com/@fumadocs/tailwind/-/tailwind-0.0.3.tgz", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.9.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@iconify/utils": ["@iconify/utils@3.1.0", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.0.tgz", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], - "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + "@img/colour": ["@img/colour@1.1.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "https://registry.npmmirror.com/@mdx-js/mdx/-/mdx-3.1.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.1.0.tgz", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], - "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "https://registry.npmmirror.com/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], + "@next/env": ["@next/env@16.1.6", "https://registry.npmmirror.com/@next/env/-/env-16.1.6.tgz", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], - "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + "@orama/orama": ["@orama/orama@3.1.18", "https://registry.npmmirror.com/@orama/orama/-/orama-3.1.18.tgz", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], - "@orama/tokenizers": ["@orama/tokenizers@3.1.18", "", { "dependencies": { "@orama/orama": "3.1.18" } }, "sha512-Ra4dFddWZ7hCGPAehnd/6QZjlzQvczYSt1y1Fq4HteJqRu2yvfR6fXvxiUnKi+HIgJYPxKhebJEjvJdF8LYQWg=="], + "@orama/tokenizers": ["@orama/tokenizers@3.1.18", "https://registry.npmmirror.com/@orama/tokenizers/-/tokenizers-3.1.18.tgz", { "dependencies": { "@orama/orama": "3.1.18" } }, "sha512-Ra4dFddWZ7hCGPAehnd/6QZjlzQvczYSt1y1Fq4HteJqRu2yvfR6fXvxiUnKi+HIgJYPxKhebJEjvJdF8LYQWg=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "https://registry.npmmirror.com/@oxc-project/runtime/-/runtime-0.115.0.tgz", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], - "@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], + "@oxc-project/types": ["@oxc-project/types@0.115.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.115.0.tgz", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], - "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], + "@phc/format": ["@phc/format@1.0.0", "https://registry.npmmirror.com/@phc/format/-/format-1.0.0.tgz", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "https://registry.npmmirror.com/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], - "@prisma/client": ["@prisma/client@7.4.2", "", { "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA=="], + "@prisma/client": ["@prisma/client@7.4.2", "https://registry.npmmirror.com/@prisma/client/-/client-7.4.2.tgz", { "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA=="], - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.4.2", "", {}, "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.4.2", "https://registry.npmmirror.com/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.2.tgz", {}, "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q=="], - "@prisma/config": ["@prisma/config@7.4.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg=="], + "@prisma/config": ["@prisma/config@7.4.2", "https://registry.npmmirror.com/@prisma/config/-/config-7.4.2.tgz", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg=="], - "@prisma/debug": ["@prisma/debug@7.4.2", "", {}, "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w=="], + "@prisma/debug": ["@prisma/debug@7.4.2", "https://registry.npmmirror.com/@prisma/debug/-/debug-7.4.2.tgz", {}, "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w=="], - "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], + "@prisma/dev": ["@prisma/dev@0.20.0", "https://registry.npmmirror.com/@prisma/dev/-/dev-0.20.0.tgz", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], - "@prisma/dmmf": ["@prisma/dmmf@7.4.2", "", {}, "sha512-EurUPjBwmAd69MM8PLZ5n94xeeQ+7SB3Wd9p2+YsgXhvqCa1cgJRBJiB6fTWkPTz5Lf05ySNJ/59EnuEJMAnrw=="], + "@prisma/dmmf": ["@prisma/dmmf@7.4.2", "https://registry.npmmirror.com/@prisma/dmmf/-/dmmf-7.4.2.tgz", {}, "sha512-EurUPjBwmAd69MM8PLZ5n94xeeQ+7SB3Wd9p2+YsgXhvqCa1cgJRBJiB6fTWkPTz5Lf05ySNJ/59EnuEJMAnrw=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.2", "https://registry.npmmirror.com/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw=="], - "@prisma/engines": ["@prisma/engines@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/fetch-engine": "7.4.2", "@prisma/get-platform": "7.4.2" } }, "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA=="], + "@prisma/engines": ["@prisma/engines@7.4.2", "https://registry.npmmirror.com/@prisma/engines/-/engines-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/fetch-engine": "7.4.2", "@prisma/get-platform": "7.4.2" } }, "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA=="], - "@prisma/engines-version": ["@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "", {}, "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg=="], + "@prisma/engines-version": ["@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", {}, "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/get-platform": "7.4.2" } }, "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.2", "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/get-platform": "7.4.2" } }, "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA=="], - "@prisma/generator": ["@prisma/generator@7.4.2", "", {}, "sha512-+4B8Rrq5Ef2cm7cW7Wyu6oXn/QVcDVdNti9mKEkcbPVLlA85I6Rbd+j6r+k2E2nlj0zKZbDbq8fIx/Xnt1SmkA=="], + "@prisma/generator": ["@prisma/generator@7.4.2", "https://registry.npmmirror.com/@prisma/generator/-/generator-7.4.2.tgz", {}, "sha512-+4B8Rrq5Ef2cm7cW7Wyu6oXn/QVcDVdNti9mKEkcbPVLlA85I6Rbd+j6r+k2E2nlj0zKZbDbq8fIx/Xnt1SmkA=="], - "@prisma/generator-helper": ["@prisma/generator-helper@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/dmmf": "7.4.2", "@prisma/generator": "7.4.2" } }, "sha512-PWTkqlMILGVghoWP8SZB3prw/EcthiyPYJsMoiVZrClqPMQn6doPjphCQffPDyGzZvAwmA64edbc6Gm/6Omkjg=="], + "@prisma/generator-helper": ["@prisma/generator-helper@7.4.2", "https://registry.npmmirror.com/@prisma/generator-helper/-/generator-helper-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/dmmf": "7.4.2", "@prisma/generator": "7.4.2" } }, "sha512-PWTkqlMILGVghoWP8SZB3prw/EcthiyPYJsMoiVZrClqPMQn6doPjphCQffPDyGzZvAwmA64edbc6Gm/6Omkjg=="], - "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], + "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.2.0.tgz", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], - "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], + "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "https://registry.npmmirror.com/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], - "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], + "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "https://registry.npmmirror.com/@prisma/studio-core/-/studio-core-0.13.1.tgz", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], @@ -349,1210 +349,1210 @@ "@project-cvsa/db": ["@project-cvsa/db@workspace:packages/db"], - "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], - "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], - "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], + "@shikijs/core": ["@shikijs/core@4.0.2", "https://registry.npmmirror.com/@shikijs/core/-/core-4.0.2.tgz", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], - "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], + "@shikijs/langs": ["@shikijs/langs@4.0.2", "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], - "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], + "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], - "@shikijs/rehype": ["@shikijs/rehype@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "4.0.2", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" } }, "sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA=="], + "@shikijs/rehype": ["@shikijs/rehype@4.0.2", "https://registry.npmmirror.com/@shikijs/rehype/-/rehype-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "4.0.2", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" } }, "sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA=="], - "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], + "@shikijs/themes": ["@shikijs/themes@4.0.2", "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.0.2.tgz", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], - "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], + "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-4.0.2.tgz", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], - "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + "@shikijs/types": ["@shikijs/types@4.0.2", "https://registry.npmmirror.com/@shikijs/types/-/types-4.0.2.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.48.tgz", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "https://registry.npmmirror.com/@t3-oss/env-core/-/env-core-0.13.10.tgz", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.2.1.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.2.1.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.2.1.tgz", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="], - "@takumi-rs/core": ["@takumi-rs/core@0.73.1", "", { "dependencies": { "@takumi-rs/helpers": "0.73.1" }, "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.73.1", "@takumi-rs/core-darwin-x64": "0.73.1", "@takumi-rs/core-linux-arm64-gnu": "0.73.1", "@takumi-rs/core-linux-arm64-musl": "0.73.1", "@takumi-rs/core-linux-x64-gnu": "0.73.1", "@takumi-rs/core-linux-x64-musl": "0.73.1", "@takumi-rs/core-win32-arm64-msvc": "0.73.1", "@takumi-rs/core-win32-x64-msvc": "0.73.1" } }, "sha512-+8GNu0O9lDNBjXx2uM3/15zIyTCt+vI3Ltmkudacybnvl6Kx2TMokRWD9/bymg19ZVcGbfOC1FsFIGQdVePCvQ=="], + "@takumi-rs/core": ["@takumi-rs/core@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core/-/core-0.73.1.tgz", { "dependencies": { "@takumi-rs/helpers": "0.73.1" }, "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.73.1", "@takumi-rs/core-darwin-x64": "0.73.1", "@takumi-rs/core-linux-arm64-gnu": "0.73.1", "@takumi-rs/core-linux-arm64-musl": "0.73.1", "@takumi-rs/core-linux-x64-gnu": "0.73.1", "@takumi-rs/core-linux-x64-musl": "0.73.1", "@takumi-rs/core-win32-arm64-msvc": "0.73.1", "@takumi-rs/core-win32-x64-msvc": "0.73.1" } }, "sha512-+8GNu0O9lDNBjXx2uM3/15zIyTCt+vI3Ltmkudacybnvl6Kx2TMokRWD9/bymg19ZVcGbfOC1FsFIGQdVePCvQ=="], - "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@0.73.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6DQZ7XM6GArr32n9k6Es3/1X0gJHEnVSdUTVUPVGRQrCmscjV07zrTNXn8VoU81XIl5uHQpRgUT9HhaLPbd/ew=="], + "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-darwin-arm64/-/core-darwin-arm64-0.73.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-6DQZ7XM6GArr32n9k6Es3/1X0gJHEnVSdUTVUPVGRQrCmscjV07zrTNXn8VoU81XIl5uHQpRgUT9HhaLPbd/ew=="], - "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@0.73.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GUggPolYTa2bAuWu3aqZZCSzCo+ZGFsLKzW+pkc6D8n+PrWCko25vbMpkNhG7Cz5a7UcuWUaTba8rBejNKF/HQ=="], + "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-darwin-x64/-/core-darwin-x64-0.73.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-GUggPolYTa2bAuWu3aqZZCSzCo+ZGFsLKzW+pkc6D8n+PrWCko25vbMpkNhG7Cz5a7UcuWUaTba8rBejNKF/HQ=="], - "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@0.73.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-NEVs6lu1MeKlZKJjOJdp3pL/lyLpOCr2QHp4zIH4eM5r4kZi2uFj4jgv4U9fqWtiwJaaSdaox4/ffkzBsYcFbw=="], + "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-linux-arm64-gnu/-/core-linux-arm64-gnu-0.73.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-NEVs6lu1MeKlZKJjOJdp3pL/lyLpOCr2QHp4zIH4eM5r4kZi2uFj4jgv4U9fqWtiwJaaSdaox4/ffkzBsYcFbw=="], - "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@0.73.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-idzxH2JcUo+yD2t7OexbPB5DN+1x88GcpDKoJVvwc2nOxr7KQGdGqALVBOe5i2mNnkUSIr/ZeBz7yBII5bA6ag=="], + "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-linux-arm64-musl/-/core-linux-arm64-musl-0.73.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-idzxH2JcUo+yD2t7OexbPB5DN+1x88GcpDKoJVvwc2nOxr7KQGdGqALVBOe5i2mNnkUSIr/ZeBz7yBII5bA6ag=="], - "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@0.73.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XSOf9b0F5lUScBcGegr8PQWuEJXeFdG7rsZLe1MvTVOzrmzqEhLnaK4xckV1ykWF5qWCf78yArqLt+C7mf+u6Q=="], + "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-linux-x64-gnu/-/core-linux-x64-gnu-0.73.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XSOf9b0F5lUScBcGegr8PQWuEJXeFdG7rsZLe1MvTVOzrmzqEhLnaK4xckV1ykWF5qWCf78yArqLt+C7mf+u6Q=="], - "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@0.73.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rHsvVVT+m4+zaIjJxp2dn8ibvjCtXRasByMii6st7zRwioQIibCAMq/Ik/uVcPFpeMBhyai6mGZe0NTWq23ZEA=="], + "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-linux-x64-musl/-/core-linux-x64-musl-0.73.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-rHsvVVT+m4+zaIjJxp2dn8ibvjCtXRasByMii6st7zRwioQIibCAMq/Ik/uVcPFpeMBhyai6mGZe0NTWq23ZEA=="], - "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@0.73.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YEA79NFEAgCI2bs0L/xRWMlBHo8Z1q+Oy/7pPxu4F7Qxy5JVGYwd3UDKnf01os2eNfdNmMI+7t0F5sderk6hiQ=="], + "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-win32-arm64-msvc/-/core-win32-arm64-msvc-0.73.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-YEA79NFEAgCI2bs0L/xRWMlBHo8Z1q+Oy/7pPxu4F7Qxy5JVGYwd3UDKnf01os2eNfdNmMI+7t0F5sderk6hiQ=="], - "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@0.73.1", "", { "os": "win32", "cpu": "x64" }, "sha512-/vluH32+cST6CBM1n1XUTjnRCkfDmQxqRrM1v8Ccpv1aKHrYdh2BRz47oY2hFiEcO4jyP36lmhPvDUS8odVaFw=="], + "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@0.73.1", "https://registry.npmmirror.com/@takumi-rs/core-win32-x64-msvc/-/core-win32-x64-msvc-0.73.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-/vluH32+cST6CBM1n1XUTjnRCkfDmQxqRrM1v8Ccpv1aKHrYdh2BRz47oY2hFiEcO4jyP36lmhPvDUS8odVaFw=="], - "@takumi-rs/helpers": ["@takumi-rs/helpers@0.73.1", "", {}, "sha512-OONKM9+XtiRp7s2e3kwbnTyF9cYqk6L8d7Q2KG449OjphdlhuCZhVwmpqHI2nHEgL1TLC2LEHzxJHvfyAIdNBw=="], + "@takumi-rs/helpers": ["@takumi-rs/helpers@0.73.1", "https://registry.npmmirror.com/@takumi-rs/helpers/-/helpers-0.73.1.tgz", {}, "sha512-OONKM9+XtiRp7s2e3kwbnTyF9cYqk6L8d7Q2KG449OjphdlhuCZhVwmpqHI2nHEgL1TLC2LEHzxJHvfyAIdNBw=="], - "@takumi-rs/image-response": ["@takumi-rs/image-response@0.73.1", "", { "dependencies": { "@takumi-rs/core": "0.73.1", "@takumi-rs/helpers": "0.73.1", "@takumi-rs/wasm": "0.73.1" } }, "sha512-5/Sl1fgnQNG8FPsWgLeHiSpkh1DACKkYxDXcc/6suJq5IhPU4l4ydo8q4aP9zVmD4QSQfu69Jy7Leabx5tonJQ=="], + "@takumi-rs/image-response": ["@takumi-rs/image-response@0.73.1", "https://registry.npmmirror.com/@takumi-rs/image-response/-/image-response-0.73.1.tgz", { "dependencies": { "@takumi-rs/core": "0.73.1", "@takumi-rs/helpers": "0.73.1", "@takumi-rs/wasm": "0.73.1" } }, "sha512-5/Sl1fgnQNG8FPsWgLeHiSpkh1DACKkYxDXcc/6suJq5IhPU4l4ydo8q4aP9zVmD4QSQfu69Jy7Leabx5tonJQ=="], - "@takumi-rs/wasm": ["@takumi-rs/wasm@0.73.1", "", { "dependencies": { "@takumi-rs/helpers": "0.73.1" } }, "sha512-WDrstyQettPQSmYDyj7lrJMKlm3E5dg0ZC+PFvKTdfP4aMTZgWBchIL3MfdUwpRIRf96sdL+pAdrwI42X9iRXg=="], + "@takumi-rs/wasm": ["@takumi-rs/wasm@0.73.1", "https://registry.npmmirror.com/@takumi-rs/wasm/-/wasm-0.73.1.tgz", { "dependencies": { "@takumi-rs/helpers": "0.73.1" } }, "sha512-WDrstyQettPQSmYDyj7lrJMKlm3E5dg0ZC+PFvKTdfP4aMTZgWBchIL3MfdUwpRIRf96sdL+pAdrwI42X9iRXg=="], - "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "https://registry.npmmirror.com/@tokenizer/inflate/-/inflate-0.4.1.tgz", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], - "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "https://registry.npmmirror.com/@tokenizer/token/-/token-0.3.0.tgz", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.3.10", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + "@types/d3": ["@types/d3@7.4.3", "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + "@types/d3-axis": ["@types/d3-axis@3.0.6", "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + "@types/d3-brush": ["@types/d3-brush@3.0.6", "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + "@types/d3-chord": ["@types/d3-chord@3.0.6", "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + "@types/d3-contour": ["@types/d3-contour@3.0.6", "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + "@types/d3-drag": ["@types/d3-drag@3.0.7", "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + "@types/d3-ease": ["@types/d3-ease@3.0.2", "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + "@types/d3-force": ["@types/d3-force@3.0.10", "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + "@types/d3-format": ["@types/d3-format@3.0.4", "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + "@types/d3-geo": ["@types/d3-geo@3.1.0", "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + "@types/d3-path": ["@types/d3-path@3.1.1", "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + "@types/d3-random": ["@types/d3-random@3.0.3", "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "@types/d3-scale": ["@types/d3-scale@4.0.9", "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + "@types/d3-selection": ["@types/d3-selection@3.0.11", "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + "@types/d3-shape": ["@types/d3-shape@3.1.8", "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + "@types/d3-time": ["@types/d3-time@3.0.4", "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/d3-timer": ["@types/d3-timer@3.0.2", "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/debug": ["@types/debug@4.1.12", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/geojson": ["@types/geojson@7946.0.16", "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/hast": ["@types/hast@3.0.4", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mdast": ["@types/mdast@4.0.4", "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/mdx": ["@types/mdx@2.0.13", "https://registry.npmmirror.com/@types/mdx/-/mdx-2.0.13.tgz", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/node": ["@types/node@25.5.0", "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], - "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], + "@types/pg": ["@types/pg@8.18.0", "https://registry.npmmirror.com/@types/pg/-/pg-8.18.0.tgz", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], + "argon2": ["argon2@0.44.0", "https://registry.npmmirror.com/argon2/-/argon2-0.44.0.tgz", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "astring": ["astring@1.9.0", "https://registry.npmmirror.com/astring/-/astring-1.9.0.tgz", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "https://registry.npmmirror.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "bail": ["bail@2.0.2", "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], - "beautiful-mermaid": ["beautiful-mermaid@1.1.3", "", { "dependencies": { "elkjs": "^0.11.0", "entities": "^7.0.1" } }, "sha512-TItrtrAyHp1vwFfFVYauWGrquouk/6SS21Aq3RsxindSYZODcN4xYrPZD6BiZRU+o5mKJzDPz9MUSMvELdylyg=="], + "beautiful-mermaid": ["beautiful-mermaid@1.1.3", "https://registry.npmmirror.com/beautiful-mermaid/-/beautiful-mermaid-1.1.3.tgz", { "dependencies": { "elkjs": "^0.11.0", "entities": "^7.0.1" } }, "sha512-TItrtrAyHp1vwFfFVYauWGrquouk/6SS21Aq3RsxindSYZODcN4xYrPZD6BiZRU+o5mKJzDPz9MUSMvELdylyg=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.10", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], - "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "c12": ["c12@3.1.0", "https://registry.npmmirror.com/c12/-/c12-3.1.0.tgz", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001777", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "ccount": ["ccount@2.0.1", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "character-entities": ["character-entities@2.0.2", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "character-entities-legacy": ["character-entities-legacy@3.0.0", "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "character-reference-invalid": ["character-reference-invalid@2.0.1", "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + "chevrotain": ["chevrotain@10.5.0", "https://registry.npmmirror.com/chevrotain/-/chevrotain-10.5.0.tgz", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], - "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], + "chevrotain-allstar": ["chevrotain-allstar@0.4.1", "https://registry.npmmirror.com/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="], - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "citty": ["citty@0.1.6", "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "client-only": ["client-only@0.0.1", "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "https://registry.npmmirror.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], - "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + "confbox": ["confbox@0.2.4", "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "consola": ["consola@3.4.2", "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + "cose-base": ["cose-base@1.0.3", "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], + "cross-env": ["cross-env@10.1.0", "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssesc": ["cssesc@3.0.0", "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], + "cytoscape": ["cytoscape@3.33.2", "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.2.tgz", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], - "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + "d3": ["d3@7.9.0", "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-array": ["d3-array@3.2.4", "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + "d3-axis": ["d3-axis@3.0.0", "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + "d3-brush": ["d3-brush@3.0.0", "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + "d3-chord": ["d3-chord@3.0.1", "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + "d3-color": ["d3-color@3.1.0", "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + "d3-contour": ["d3-contour@4.0.2", "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + "d3-delaunay": ["d3-delaunay@6.0.4", "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + "d3-dispatch": ["d3-dispatch@3.0.1", "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + "d3-drag": ["d3-drag@3.0.0", "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + "d3-dsv": ["d3-dsv@3.0.1", "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + "d3-ease": ["d3-ease@3.0.1", "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + "d3-fetch": ["d3-fetch@3.0.1", "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + "d3-force": ["d3-force@3.0.0", "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + "d3-format": ["d3-format@3.1.2", "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + "d3-geo": ["d3-geo@3.1.1", "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + "d3-hierarchy": ["d3-hierarchy@3.1.2", "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + "d3-path": ["d3-path@3.1.0", "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + "d3-polygon": ["d3-polygon@3.0.1", "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + "d3-quadtree": ["d3-quadtree@3.0.1", "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + "d3-random": ["d3-random@3.0.1", "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + "d3-sankey": ["d3-sankey@0.12.3", "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + "d3-scale": ["d3-scale@4.0.2", "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + "d3-selection": ["d3-selection@3.0.0", "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + "d3-shape": ["d3-shape@3.2.0", "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + "d3-time": ["d3-time@3.1.0", "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + "d3-time-format": ["d3-time-format@4.1.0", "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + "d3-transition": ["d3-transition@3.0.1", "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "d3-zoom": ["d3-zoom@3.0.0", "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "dagre-d3-es": ["dagre-d3-es@7.0.14", "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + "dayjs": ["dayjs@1.11.20", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], - "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + "debug": ["debug@4.3.4", "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], - "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "delaunator": ["delaunator@5.1.0", "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], - "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "denque": ["denque@2.1.0", "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "destr": ["destr@2.0.5", "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "devlop": ["devlop@1.1.0", "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "docs": ["docs@workspace:apps/docs"], - "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "dompurify": ["dompurify@3.3.3", "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.3.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@16.6.1", "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], + "effect": ["effect@3.18.4", "https://registry.npmmirror.com/effect/-/effect-3.18.4.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], - "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="], + "elkjs": ["elkjs@0.11.1", "https://registry.npmmirror.com/elkjs/-/elkjs-0.11.1.tgz", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="], - "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], + "elysia": ["elysia@1.4.27", "https://registry.npmmirror.com/elysia/-/elysia-1.4.27.tgz", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], - "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], + "elysia-ip": ["elysia-ip@1.0.10", "https://registry.npmmirror.com/elysia-ip/-/elysia-ip-1.0.10.tgz", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], - "elysia-rate-limit": ["elysia-rate-limit@4.5.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-nsbl3WLvrGiG/SdTgevAsjCUJhY34Bgf+7bDOYrjTPZyS7Hd4MLuLc4MUr9TFsSJWPvKpzyrX2HW8IWjRbex1Q=="], + "elysia-rate-limit": ["elysia-rate-limit@4.5.0", "https://registry.npmmirror.com/elysia-rate-limit/-/elysia-rate-limit-4.5.0.tgz", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-nsbl3WLvrGiG/SdTgevAsjCUJhY34Bgf+7bDOYrjTPZyS7Hd4MLuLc4MUr9TFsSJWPvKpzyrX2HW8IWjRbex1Q=="], - "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "empathic": ["empathic@2.0.0", "https://registry.npmmirror.com/empathic/-/empathic-2.0.0.tgz", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], - "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + "enhanced-resolve": ["enhanced-resolve@5.20.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "https://registry.npmmirror.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], - "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "esast-util-from-js": ["esast-util-from-js@2.0.1", "https://registry.npmmirror.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "esbuild": ["esbuild@0.27.3", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "https://registry.npmmirror.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], - "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "https://registry.npmmirror.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + "estree-util-scope": ["estree-util-scope@1.0.0", "https://registry.npmmirror.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], - "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + "estree-util-to-js": ["estree-util-to-js@2.0.0", "https://registry.npmmirror.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "https://registry.npmmirror.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + "estree-util-visit": ["estree-util-visit@2.0.0", "https://registry.npmmirror.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + "exact-mirror": ["exact-mirror@0.2.7", "https://registry.npmmirror.com/exact-mirror/-/exact-mirror-0.2.7.tgz", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], - "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "exsolve": ["exsolve@1.0.8", "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "extend": ["extend@3.0.2", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-check": ["fast-check@3.23.2", "https://registry.npmmirror.com/fast-check/-/fast-check-3.23.2.tgz", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "file-type": ["file-type@21.3.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA=="], + "file-type": ["file-type@21.3.1", "https://registry.npmmirror.com/file-type/-/file-type-21.3.1.tgz", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "foreground-child": ["foreground-child@3.3.1", "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "framer-motion": ["framer-motion@12.37.0", "", { "dependencies": { "motion-dom": "^12.37.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-j/QUcZS9Nw3NzZWoAbkzr3ETRFHyVeQMlGOUYUmG15U+uiyn9DqIktYruVPDcqY8I35qYR70JaZBvFmS6p+Pdg=="], + "framer-motion": ["framer-motion@12.37.0", "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.37.0.tgz", { "dependencies": { "motion-dom": "^12.37.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-j/QUcZS9Nw3NzZWoAbkzr3ETRFHyVeQMlGOUYUmG15U+uiyn9DqIktYruVPDcqY8I35qYR70JaZBvFmS6p+Pdg=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fumadocs-core": ["fumadocs-core@16.6.17", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-ssHz9a7+ZZSkHjB4/sfHq9rO2fPW8jtw2fPeDVzkPJd34DqOPbxuaP0TQ6CEs1Pei99Fky9CzE8ENS3H8WFxnQ=="], + "fumadocs-core": ["fumadocs-core@16.6.17", "https://registry.npmmirror.com/fumadocs-core/-/fumadocs-core-16.6.17.tgz", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-ssHz9a7+ZZSkHjB4/sfHq9rO2fPW8jtw2fPeDVzkPJd34DqOPbxuaP0TQ6CEs1Pei99Fky9CzE8ENS3H8WFxnQ=="], - "fumadocs-mdx": ["fumadocs-mdx@14.2.10", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.3", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-0gITZiJb92c7xJwSMdcGBEY2+pFcRvklSNwxIAMTy4gjnuLZANjaXKw+qJ6E5+s9dO0IGlimHv5zyMYLjReg0w=="], + "fumadocs-mdx": ["fumadocs-mdx@14.2.10", "https://registry.npmmirror.com/fumadocs-mdx/-/fumadocs-mdx-14.2.10.tgz", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.3", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-0gITZiJb92c7xJwSMdcGBEY2+pFcRvklSNwxIAMTy4gjnuLZANjaXKw+qJ6E5+s9dO0IGlimHv5zyMYLjReg0w=="], - "fumadocs-ui": ["fumadocs-ui@16.6.17", "", { "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.577.0", "motion": "^12.36.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.1", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.6.17", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-RLr1Dsujq3YoOEi4cLu52mZkT8fBJUl1rq4DtVoQWhvk20WYl1aDxlBhMr4guAvG5Malwh6Vy1QJ5KbE/k2E6w=="], + "fumadocs-ui": ["fumadocs-ui@16.6.17", "https://registry.npmmirror.com/fumadocs-ui/-/fumadocs-ui-16.6.17.tgz", { "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.577.0", "motion": "^12.36.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.1", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.6.17", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-RLr1Dsujq3YoOEi4cLu52mZkT8fBJUl1rq4DtVoQWhvk20WYl1aDxlBhMr4guAvG5Malwh6Vy1QJ5KbE/k2E6w=="], - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "generate-function": ["generate-function@2.3.1", "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-nonce": ["get-nonce@1.0.1", "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + "get-port-please": ["get-port-please@3.2.0", "https://registry.npmmirror.com/get-port-please/-/get-port-please-3.2.0.tgz", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], - "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "giget": ["giget@2.0.0", "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + "github-slugger": ["github-slugger@2.0.0", "https://registry.npmmirror.com/github-slugger/-/github-slugger-2.0.0.tgz", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], + "grammex": ["grammex@3.1.12", "https://registry.npmmirror.com/grammex/-/grammex-3.1.12.tgz", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], - "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], + "graphmatch": ["graphmatch@1.1.1", "https://registry.npmmirror.com/graphmatch/-/graphmatch-1.1.1.tgz", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], - "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "hachure-fill": ["hachure-fill@0.5.2", "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + "hast-util-raw": ["hast-util-raw@9.1.0", "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "https://registry.npmmirror.com/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + "hast-util-to-string": ["hast-util-to-string@3.0.1", "https://registry.npmmirror.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hastscript": ["hastscript@9.0.1", "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "hono": ["hono@4.11.4", "https://registry.npmmirror.com/hono/-/hono-4.11.4.tgz", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "html-void-elements": ["html-void-elements@3.0.0", "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + "http-status-codes": ["http-status-codes@2.3.0", "https://registry.npmmirror.com/http-status-codes/-/http-status-codes-2.3.0.tgz", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + "image-size": ["image-size@2.0.2", "https://registry.npmmirror.com/image-size/-/image-size-2.0.2.tgz", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "internmap": ["internmap@1.0.1", "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-alphanumerical": ["is-alphanumerical@2.0.1", "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-decimal": ["is-decimal@2.0.1", "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-property": ["is-property@1.0.2", "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + "katex": ["katex@0.16.45", "https://registry.npmmirror.com/katex/-/katex-0.16.45.tgz", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], - "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], + "langium": ["langium@4.2.2", "https://registry.npmmirror.com/langium/-/langium-4.2.2.tgz", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="], - "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "layout-base": ["layout-base@1.0.2", "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + "lightningcss": ["lightningcss@1.31.1", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.31.1.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "lilconfig": ["lilconfig@2.1.0", "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.21", "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "lru.min": ["lru.min@1.1.4", "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], + "lucide-react": ["lucide-react@0.577.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.577.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "markdown-table": ["markdown-table@3.0.4", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "marked": ["marked@16.4.2", "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "https://registry.npmmirror.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "https://registry.npmmirror.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "https://registry.npmmirror.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "https://registry.npmmirror.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "https://registry.npmmirror.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "memoirist": ["memoirist@0.4.0", "https://registry.npmmirror.com/memoirist/-/memoirist-0.4.0.tgz", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], - "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], + "mermaid": ["mermaid@11.14.0", "https://registry.npmmirror.com/mermaid/-/mermaid-11.14.0.tgz", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + "micromark": ["micromark@4.0.2", "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "https://registry.npmmirror.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "https://registry.npmmirror.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "https://registry.npmmirror.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], - "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], - "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + "micromark-factory-label": ["micromark-factory-label@2.0.1", "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "https://registry.npmmirror.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + "micromark-factory-space": ["micromark-factory-space@2.0.1", "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + "micromark-factory-title": ["micromark-factory-title@2.0.1", "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + "micromark-util-encode": ["micromark-util-encode@2.0.1", "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "https://registry.npmmirror.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromark-util-types": ["micromark-util-types@2.0.2", "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "mlly": ["mlly@1.8.2", "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "motion": ["motion@12.37.0", "", { "dependencies": { "framer-motion": "^12.37.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Ph6oyO5hGSIAPjDsqwchEP+EKXjyFK0ci6FTIFBbx+qaMl8zLzLzPLzd9q3DKhAHcvnV7LxQonMyA+FyAv9+gA=="], + "motion": ["motion@12.37.0", "https://registry.npmmirror.com/motion/-/motion-12.37.0.tgz", { "dependencies": { "framer-motion": "^12.37.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Ph6oyO5hGSIAPjDsqwchEP+EKXjyFK0ci6FTIFBbx+qaMl8zLzLzPLzd9q3DKhAHcvnV7LxQonMyA+FyAv9+gA=="], - "motion-dom": ["motion-dom@12.37.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-LnppZuwX1jQizRWTl9LBLMN3RbAEmdQkX/2Af0UW70NCqYJI/7GfI83vQP9Ucel/Avc0Tf2ZWy8FHawuc0O6Vg=="], + "motion-dom": ["motion-dom@12.37.0", "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.37.0.tgz", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-LnppZuwX1jQizRWTl9LBLMN3RbAEmdQkX/2Af0UW70NCqYJI/7GfI83vQP9Ucel/Avc0Tf2ZWy8FHawuc0O6Vg=="], - "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "motion-utils": ["motion-utils@12.36.0", "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.36.0.tgz", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + "ms": ["ms@2.1.2", "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], + "mysql2": ["mysql2@3.15.3", "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.3.tgz", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], - "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "named-placeholders": ["named-placeholders@1.1.6", "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanoid": ["nanoid@5.1.6", "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.6.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], + "next": ["next@16.1.6", "https://registry.npmmirror.com/next/-/next-16.1.6.tgz", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "node-addon-api": ["node-addon-api@8.6.0", "", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], + "node-addon-api": ["node-addon-api@8.6.0", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.6.0.tgz", {}, "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "npm-to-yarn": ["npm-to-yarn@3.0.1", "", {}, "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A=="], + "npm-to-yarn": ["npm-to-yarn@3.0.1", "https://registry.npmmirror.com/npm-to-yarn/-/npm-to-yarn-3.0.1.tgz", {}, "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "nypm": ["nypm@0.6.5", "https://registry.npmmirror.com/nypm/-/nypm-0.6.5.tgz", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "ohash": ["ohash@2.0.11", "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "openapi-types": ["openapi-types@12.1.3", "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-entities": ["parse-entities@4.0.2", "https://registry.npmmirror.com/parse-entities/-/parse-entities-4.0.2.tgz", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-data-parser": ["path-data-parser@0.1.0", "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "perfect-debounce": ["perfect-debounce@1.0.0", "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], - "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], + "pg": ["pg@8.20.0", "https://registry.npmmirror.com/pg/-/pg-8.20.0.tgz", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], - "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + "pg-cloudflare": ["pg-cloudflare@1.3.0", "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + "pg-connection-string": ["pg-connection-string@2.12.0", "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + "pg-int8": ["pg-int8@1.0.1", "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], + "pg-pool": ["pg-pool@3.13.0", "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.13.0.tgz", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], - "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + "pg-protocol": ["pg-protocol@1.13.0", "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.13.0.tgz", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], - "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "pg-types": ["pg-types@2.2.0", "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "pgpass": ["pgpass@1.0.5", "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pkg-types": ["pkg-types@2.3.0", "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + "points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "points-on-path": ["points-on-path@0.2.1", "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.8", "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], - "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], - "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + "postgres": ["postgres@3.4.7", "https://registry.npmmirror.com/postgres/-/postgres-3.4.7.tgz", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], - "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], + "postgres-array": ["postgres-array@3.0.4", "https://registry.npmmirror.com/postgres-array/-/postgres-array-3.0.4.tgz", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], - "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + "postgres-bytea": ["postgres-bytea@1.0.1", "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], - "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + "postgres-date": ["postgres-date@1.0.7", "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "postgres-interval": ["postgres-interval@1.2.0", "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prisma": ["prisma@7.4.2", "", { "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA=="], + "prisma": ["prisma@7.4.2", "https://registry.npmmirror.com/prisma/-/prisma-7.4.2.tgz", { "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA=="], - "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], + "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "https://registry.npmmirror.com/prisma-json-types-generator/-/prisma-json-types-generator-4.1.1.tgz", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "property-information": ["property-information@7.1.0", "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pure-rand": ["pure-rand@6.1.0", "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "rc9": ["rc9@2.1.2", "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.4", "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.4", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-medium-image-zoom": ["react-medium-image-zoom@5.4.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g=="], + "react-medium-image-zoom": ["react-medium-image-zoom@5.4.1", "https://registry.npmmirror.com/react-medium-image-zoom/-/react-medium-image-zoom-5.4.1.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g=="], - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], + "react-router": ["react-router@7.13.1", "https://registry.npmmirror.com/react-router/-/react-router-7.13.1.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "https://registry.npmmirror.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], - "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + "recma-jsx": ["recma-jsx@1.0.1", "https://registry.npmmirror.com/recma-jsx/-/recma-jsx-1.0.1.tgz", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], - "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + "recma-parse": ["recma-parse@1.0.0", "https://registry.npmmirror.com/recma-parse/-/recma-parse-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], - "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "recma-stringify": ["recma-stringify@1.0.0", "https://registry.npmmirror.com/recma-stringify/-/recma-stringify-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + "regex": ["regex@6.1.0", "https://registry.npmmirror.com/regex/-/regex-6.1.0.tgz", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + "regex-recursion": ["regex-recursion@6.0.2", "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regex-utilities": ["regex-utilities@2.3.0", "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + "regexp-to-ast": ["regexp-to-ast@0.5.0", "https://registry.npmmirror.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + "rehype-raw": ["rehype-raw@7.0.0", "https://registry.npmmirror.com/rehype-raw/-/rehype-raw-7.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + "rehype-recma": ["rehype-recma@1.0.0", "https://registry.npmmirror.com/rehype-recma/-/rehype-recma-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], - "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], + "remark": ["remark@15.0.1", "https://registry.npmmirror.com/remark/-/remark-15.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-gfm": ["remark-gfm@4.0.1", "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + "remark-mdx": ["remark-mdx@3.1.1", "https://registry.npmmirror.com/remark-mdx/-/remark-mdx-3.1.1.tgz", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + "remark-parse": ["remark-parse@11.0.0", "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "remark-rehype": ["remark-rehype@11.1.2", "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-11.1.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "remark-stringify": ["remark-stringify@11.0.0", "https://registry.npmmirror.com/remark-stringify/-/remark-stringify-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "remeda": ["remeda@2.33.4", "https://registry.npmmirror.com/remeda/-/remeda-2.33.4.tgz", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + "robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], + "rolldown": ["rolldown@1.0.0-rc.9", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.9.tgz", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], - "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "rw": ["rw@1.3.3", "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "seq-queue": ["seq-queue@0.0.5", "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + "shiki": ["shiki@4.0.2", "https://registry.npmmirror.com/shiki/-/shiki-4.0.2.tgz", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map": ["source-map@0.7.6", "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "source-map-support": ["source-map-support@0.5.21", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "split2": ["split2@4.2.0", "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "sqlstring": ["sqlstring@2.3.3", "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "std-env": ["std-env@3.10.0", "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stringify-entities": ["stringify-entities@4.0.4", "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "strtok3": ["strtok3@10.3.4", "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.4.tgz", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + "style-to-js": ["style-to-js@1.1.21", "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "style-to-object": ["style-to-object@1.0.14", "https://registry.npmmirror.com/style-to-object/-/style-to-object-1.0.14.tgz", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "styled-jsx": ["styled-jsx@5.1.6", "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "stylis": ["stylis@4.3.6", "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], - "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + "tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], - "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + "tailwindcss": ["tailwindcss@4.2.1", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.1.tgz", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tapable": ["tapable@2.3.0", "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="], + "terser": ["terser@5.16.9", "https://registry.npmmirror.com/terser/-/terser-5.16.9.tgz", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyexec": ["tinyexec@1.0.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "token-types": ["token-types@6.1.2", "https://registry.npmmirror.com/token-types/-/token-types-6.1.2.tgz", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "trough": ["trough@2.2.0", "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "try": ["try@1.0.3", "", {}, "sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA=="], + "try": ["try@1.0.3", "https://registry.npmmirror.com/try/-/try-1.0.3.tgz", {}, "sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-dedent": ["ts-dedent@2.2.0", "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.8.17", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.17", "turbo-darwin-arm64": "2.8.17", "turbo-linux-64": "2.8.17", "turbo-linux-arm64": "2.8.17", "turbo-windows-64": "2.8.17", "turbo-windows-arm64": "2.8.17" }, "bin": { "turbo": "bin/turbo" } }, "sha512-YwPsNSqU2f/RXU/+Kcb7cPkPZARxom4+me7LKEdN5jsvy2tpfze3zDZ4EiGrJnvOm9Avu9rK0aaYsP7qZ3iz7A=="], + "turbo": ["turbo@2.8.17", "https://registry.npmmirror.com/turbo/-/turbo-2.8.17.tgz", { "optionalDependencies": { "turbo-darwin-64": "2.8.17", "turbo-darwin-arm64": "2.8.17", "turbo-linux-64": "2.8.17", "turbo-linux-arm64": "2.8.17", "turbo-windows-64": "2.8.17", "turbo-windows-arm64": "2.8.17" }, "bin": { "turbo": "bin/turbo" } }, "sha512-YwPsNSqU2f/RXU/+Kcb7cPkPZARxom4+me7LKEdN5jsvy2tpfze3zDZ4EiGrJnvOm9Avu9rK0aaYsP7qZ3iz7A=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZFkv2hv7zHpAPEXBF6ouRRXshllOavYc+jjcrYyVHvxVTTwJWsBZwJ/gpPzmOKGvkSjsEyDO5V6aqqtZzwVF+Q=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.17", "https://registry.npmmirror.com/turbo-darwin-64/-/turbo-darwin-64-2.8.17.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-ZFkv2hv7zHpAPEXBF6ouRRXshllOavYc+jjcrYyVHvxVTTwJWsBZwJ/gpPzmOKGvkSjsEyDO5V6aqqtZzwVF+Q=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5DXqhQUt24ycEryXDfMNKEkW5TBHs+QmU23a2qxXwwFDaJsWcPo2obEhBxxdEPOv7qmotjad+09RGeWCcJ9JDw=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.17", "https://registry.npmmirror.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.17.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-5DXqhQUt24ycEryXDfMNKEkW5TBHs+QmU23a2qxXwwFDaJsWcPo2obEhBxxdEPOv7qmotjad+09RGeWCcJ9JDw=="], - "turbo-linux-64": ["turbo-linux-64@2.8.17", "", { "os": "linux", "cpu": "x64" }, "sha512-KLUbz6w7F73D/Ihh51hVagrKR0/CTsPEbRkvXLXvoND014XJ4BCrQUqSxlQ4/hu+nqp1v5WlM85/h3ldeyujuA=="], + "turbo-linux-64": ["turbo-linux-64@2.8.17", "https://registry.npmmirror.com/turbo-linux-64/-/turbo-linux-64-2.8.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-KLUbz6w7F73D/Ihh51hVagrKR0/CTsPEbRkvXLXvoND014XJ4BCrQUqSxlQ4/hu+nqp1v5WlM85/h3ldeyujuA=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-pJK67XcNJH40lTAjFu7s/rUlobgVXyB3A3lDoq+/JccB3hf+SysmkpR4Itlc93s8LEaFAI4mamhFuTV17Z6wOg=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.17", "https://registry.npmmirror.com/turbo-linux-arm64/-/turbo-linux-arm64-2.8.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-pJK67XcNJH40lTAjFu7s/rUlobgVXyB3A3lDoq+/JccB3hf+SysmkpR4Itlc93s8LEaFAI4mamhFuTV17Z6wOg=="], - "turbo-windows-64": ["turbo-windows-64@2.8.17", "", { "os": "win32", "cpu": "x64" }, "sha512-EijeQ6zszDMmGZLP2vT2RXTs/GVi9rM0zv2/G4rNu2SSRSGFapgZdxgW4b5zUYLVaSkzmkpWlGfPfj76SW9yUg=="], + "turbo-windows-64": ["turbo-windows-64@2.8.17", "https://registry.npmmirror.com/turbo-windows-64/-/turbo-windows-64-2.8.17.tgz", { "os": "win32", "cpu": "x64" }, "sha512-EijeQ6zszDMmGZLP2vT2RXTs/GVi9rM0zv2/G4rNu2SSRSGFapgZdxgW4b5zUYLVaSkzmkpWlGfPfj76SW9yUg=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-crpfeMPkfECd4V1PQ/hMoiyVcOy04+bWedu/if89S15WhOalHZ2BYUi6DOJhZrszY+mTT99OwpOsj4wNfb/GHQ=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.17", "https://registry.npmmirror.com/turbo-windows-arm64/-/turbo-windows-arm64-2.8.17.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-crpfeMPkfECd4V1PQ/hMoiyVcOy04+bWedu/if89S15WhOalHZ2BYUi6DOJhZrszY+mTT99OwpOsj4wNfb/GHQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unified": ["unified@11.0.5", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + "unist-util-is": ["unist-util-is@6.0.1", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-position": ["unist-util-position@5.0.0", "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "https://registry.npmmirror.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "https://registry.npmmirror.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + "unist-util-visit": ["unist-util-visit@5.1.0", "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sidecar": ["use-sidecar@1.1.3", "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "uuid": ["uuid@11.1.0", "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "valibot": ["valibot@1.2.0", "https://registry.npmmirror.com/valibot/-/valibot-1.2.0.tgz", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + "vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + "vfile-location": ["vfile-location@5.0.3", "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="], + "vite": ["vite@8.0.0", "https://registry.npmmirror.com/vite/-/vite-8.0.0.tgz", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + "vscode-languageserver": ["vscode-languageserver@9.0.1", "https://registry.npmmirror.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "https://registry.npmmirror.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "vscode-uri": ["vscode-uri@3.1.0", "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "xtend": ["xtend@4.0.2", "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yaml": ["yaml@2.8.2", "https://registry.npmmirror.com/yaml/-/yaml-2.8.2.tgz", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], + "zeptomatch": ["zeptomatch@2.1.0", "https://registry.npmmirror.com/zeptomatch/-/zeptomatch-2.1.0.tgz", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.4.2", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw=="], - "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "https://registry.npmmirror.com/@prisma/debug/-/debug-7.2.0.tgz", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@tokenizer/inflate/debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@types/pg/@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "@types/pg/@types/node": ["@types/node@25.4.0", "https://registry.npmmirror.com/@types/node/-/node-25.4.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], - "bun-types/@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "bun-types/@types/node": ["@types/node@25.4.0", "https://registry.npmmirror.com/@types/node/-/node-25.4.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], - "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "c12/chokidar": ["chokidar@4.0.3", "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chevrotain-allstar/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + "chevrotain-allstar/chevrotain": ["chevrotain@12.0.0", "https://registry.npmmirror.com/chevrotain/-/chevrotain-12.0.0.tgz", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "d3-dsv/commander": ["commander@7.2.0", "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + "d3-sankey/d3-array": ["d3-array@2.12.1", "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "langium/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], + "langium/chevrotain": ["chevrotain@12.0.0", "https://registry.npmmirror.com/chevrotain/-/chevrotain-12.0.0.tgz", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "mlly/pkg-types": ["pkg-types@1.3.1", "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "next/postcss": ["postcss@8.4.31", "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "nypm/citty": ["citty@0.2.1", "https://registry.npmmirror.com/citty/-/citty-0.2.1.tgz", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "postcss/nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-support/source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "terser/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "vite/lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "c12/chokidar/readdirp": ["readdirp@4.1.2", "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "chevrotain-allstar/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + "chevrotain-allstar/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], - "chevrotain-allstar/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + "chevrotain-allstar/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "https://registry.npmmirror.com/@chevrotain/gast/-/gast-12.0.0.tgz", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], - "chevrotain-allstar/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + "chevrotain-allstar/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "https://registry.npmmirror.com/@chevrotain/types/-/types-12.0.0.tgz", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], - "chevrotain-allstar/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "chevrotain-allstar/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "https://registry.npmmirror.com/@chevrotain/utils/-/utils-12.0.0.tgz", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], + "langium/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], - "langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], + "langium/chevrotain/@chevrotain/gast": ["@chevrotain/gast@12.0.0", "https://registry.npmmirror.com/@chevrotain/gast/-/gast-12.0.0.tgz", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], - "langium/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], + "langium/chevrotain/@chevrotain/types": ["@chevrotain/types@12.0.0", "https://registry.npmmirror.com/@chevrotain/types/-/types-12.0.0.tgz", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], - "langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "langium/chevrotain/@chevrotain/utils": ["@chevrotain/utils@12.0.0", "https://registry.npmmirror.com/@chevrotain/utils/-/utils-12.0.0.tgz", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], - "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "next/postcss/nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], } } From ba6bb44194fac4a79e7ee6125201864f09797c4c Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 20:20:25 +0800 Subject: [PATCH 21/37] docs(guide): update env configuration instructions revise the notes about manual env config and variable consistency requirements --- apps/docs/content/internal/developers/guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/internal/developers/guide.md b/apps/docs/content/internal/developers/guide.md index fc1d849..14d9d59 100644 --- a/apps/docs/content/internal/developers/guide.md +++ b/apps/docs/content/internal/developers/guide.md @@ -56,8 +56,8 @@ MEILI_API_URL=http://localhost:7700 REDIS_URL=redis://localhost:6379 ``` -如果`.env`和`.env.test`中缺少配置项或参数,可以手动添加,但不要修改`.ts`文件中的默认值。 -`MEILI_MASTER_KEY` 通常不需要更改,保持默认值即可。`cvsa`和`password`需要和`.env.docker`文件中的值一致。 +如果`.env`和`.env.test`中缺少`REDIS_URL`可以手动添加(通常只需要在`/packages/core/`中的`.env`和`.env.test`添加)。不要修改`.ts`文件中的默认值。 +`MEILI_MASTER_KEY`、`cvsa`和`password`如需修改需要和`.env.docker`文件中的值一致。 ## 启动外部服务 From 95ef7a6657bceba08959b49d33c45c9c7d42477d Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 20:33:00 +0800 Subject: [PATCH 22/37] docs: fix 404 page and update dev guide wording 1. remove empty og:image field from not-found page 2. correct the count of containers to 3 instead of 2 in dev guide --- apps/docs/content/internal/developers/guide.md | 2 +- apps/docs/content/not-found.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/docs/content/internal/developers/guide.md b/apps/docs/content/internal/developers/guide.md index 14d9d59..82a5690 100644 --- a/apps/docs/content/internal/developers/guide.md +++ b/apps/docs/content/internal/developers/guide.md @@ -62,7 +62,7 @@ REDIS_URL=redis://localhost:6379 ## 启动外部服务 你需要准备一个 PostgreSQL 18 、Redis 7 以及 MeiliSearch 1.40 实例来辅助开发和调试。推荐使用 Docker,因为它易于管理和隔离。 -确保你安装了 [Docker Compose](https://docs.docker.com/compose/install/),之后便可以在根路径下通过 compose 拉起这两个容器。 +确保你安装了 [Docker Compose](https://docs.docker.com/compose/install/),之后便可以在根路径下通过 compose 拉起这三个容器。 ```bash docker compose up -d diff --git a/apps/docs/content/not-found.md b/apps/docs/content/not-found.md index 15f4490..5a13d4b 100644 --- a/apps/docs/content/not-found.md +++ b/apps/docs/content/not-found.md @@ -1,7 +1,6 @@ --- title: 抱歉,您访问的页面不存在。 og:title: 页面不存在 -og:image: --- 看起来我们要找的内容暂时“走丢”了,或者该链接已经失效。 From a4fc078549d59dd08a4a8813a9d8fd0893fe6d8d Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 21:56:15 +0800 Subject: [PATCH 23/37] docs(website): add robots, manifest and sitemap for docs site Enhance documentation site SEO configuration: add robots.txt, refine site manifest, and generate a complete sitemap. --- apps/docs/public/robots.txt | 4 ++ apps/docs/public/site.webmanifest | 20 +++++- apps/docs/public/sitemap.xml | 111 ++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 apps/docs/public/robots.txt create mode 100644 apps/docs/public/sitemap.xml diff --git a/apps/docs/public/robots.txt b/apps/docs/public/robots.txt new file mode 100644 index 0000000..e2b08c0 --- /dev/null +++ b/apps/docs/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://docs.projectcvsa.com/sitemap.xml \ No newline at end of file diff --git a/apps/docs/public/site.webmanifest b/apps/docs/public/site.webmanifest index 45dc8a2..f64269c 100644 --- a/apps/docs/public/site.webmanifest +++ b/apps/docs/public/site.webmanifest @@ -1 +1,19 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "Project CVSA - 中V档案馆", + "short_name": "CVSA", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/apps/docs/public/sitemap.xml b/apps/docs/public/sitemap.xml new file mode 100644 index 0000000..4749086 --- /dev/null +++ b/apps/docs/public/sitemap.xml @@ -0,0 +1,111 @@ + + + + + + + https://docs.projectcvsa.com/ + 2026-05-16T13:46:34+00:00 + 1.00 + + + https://docs.projectcvsa.com/en + 2026-05-16T13:46:34+00:00 + 0.80 + + + https://docs.projectcvsa.com/zh + 2026-05-16T13:46:34+00:00 + 0.64 + + + https://docs.projectcvsa.com/zh/scope-of-inclusion + 2026-05-16T13:46:34+00:00 + 0.51 + + + https://docs.projectcvsa.com/zh/internal + 2026-05-16T13:46:34+00:00 + 0.51 + + + https://docs.projectcvsa.com/zh/internal/developers/guide + 2026-05-16T13:46:34+00:00 + 0.41 + + + https://docs.projectcvsa.com/zh/internal/developers/architecture + 2026-05-16T13:46:34+00:00 + 0.41 + + + https://docs.projectcvsa.com/zh/internal/developers/hld + 2026-05-16T13:46:34+00:00 + 0.33 + + + https://docs.projectcvsa.com/zh/internal/developers/style-guide + 2026-05-16T13:46:34+00:00 + 0.33 + + + https://docs.projectcvsa.com/zh/internal/developers/workflow + 2026-05-16T13:46:34+00:00 + 0.33 + + + https://docs.projectcvsa.com/zh/internal/developers/crawler/overview + 2026-05-16T13:46:34+00:00 + 0.26 + + + https://docs.projectcvsa.com/zh/internal/legacy/architecture-overview + 2026-05-16T13:46:34+00:00 + 0.21 + + + https://docs.projectcvsa.com/zh/internal/legacy/crawling-lifecycle + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/video-discovery-pipeline + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/song-collection-pipeline + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/snapshot-scheduling + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/snapshot-execution + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/message-queue-reference + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/database-schema-reference + 2026-05-16T13:46:34+00:00 + 0.17 + + + https://docs.projectcvsa.com/zh/internal/legacy/network-proxy-architecture + 2026-05-16T13:46:34+00:00 + 0.17 + + + + \ No newline at end of file From f7a6df843a276b539deea1a1088892e2c75df86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BF=A2=E8=BE=B0?= <99777243+Aibeto@users.noreply.github.com> Date: Sat, 16 May 2026 22:23:22 +0800 Subject: [PATCH 24/37] chore: commit message rule for TRAE (#56) This commit adds the formal Conventional Commits based git commit guideline for the archive project, including basic format, examples, allowed commit types and PR template. --- .trae/rules/git-commit-message.md | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .trae/rules/git-commit-message.md diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..324d068 --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,56 @@ +--- +alwaysApply: true +scene: git_message +--- + +## 提交信息 + +项目采用 Conventional Commits 规范来格式化提交信息。 + +### 基本格式 + +每条提交信息应遵循以下结构(只允许使用英文): + +```text +<类型>(<作用域>): <描述> + +[正文] + +[脚注] +``` + +- **类型**:必填,表示本次提交的类别(见下文“类型列表”)。 +- **作用域**:可选,用括号包裹,指明本次提交影响的代码范围(如模块名、组件名等)。 +- **描述**:必填,是对变更的简短说明,使用祈使句、现在时态,首字母小写,末尾不加句号。 +- **正文**:可选,对变更动机及与之前行为的对比进行详细说明。 +- **脚注**:可选,主要用于关联 Issue 或标记破坏性变更(BREAKING CHANGE)。 + +### 示例 + +1. 新增一个用户登录功能: + + ```text + feat(auth): add user login endpoint + ``` + +2. 包含破坏性变更的提交(需在脚注中标记): + + ```text + feat(api): change response format for user details + + BREAKING CHANGE: The 'user' field in the response is now nested under 'data'. + ``` + +### 允许的类型列表 + +- `feat`:新增功能。 +- `fix`:修复 bug。 +- `update`: 对代码内容做出调整,而原先的代码不视为 bug(如修改前端按钮的文案) +- `docs`:仅修改文档。 +- `style`:修改代码格式(不影响代码运行的变动,如空格、分号缺失等)。 +- `refactor`:重构代码(既不是新增功能,也不是修改 bug 的代码变动)。 +- `perf`:优化性能。 +- `test`:增加测试或修改现有测试。 +- `ci`:修改 CI 配置文件或脚本(例如:Travis, Circle, BrowserStack, SauceLabs)。 +- `chore`:其他不修改源代码或测试文件的更改。 +- `revert`:撤销之前的提交。 From 3f745f783dea5d728d0b9131ec30d08fbf1b6be9 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Sat, 16 May 2026 22:45:13 +0800 Subject: [PATCH 25/37] feat(docs): add SEO optimization across landing and docs pages - Add robots.txt and sitemap generation script for search engine discoverability - Replace hardcoded with Next.js Metadata API on landing page - Add Open Graph and Twitter Card metadata to landing and docs pages - Add canonical URLs and JSON-LD structured data - Add sr-only description text on landing page for search engine indexing - Remove client-side document.title setting (now handled by Metadata API) - Improve 404 page with metadata, noindex directive, and homepage link - Update build script to generate sitemap.xml after static export --- .../app/(docs)/[lang]/[[...slug]]/page.tsx | 15 +++++++++++++++ apps/docs/app/not-found.tsx | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index 21fd616..ca0d062 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -83,15 +83,30 @@ export async function generateMetadata({ if (!page) notFound(); const cleanSlug = slug ? slug : []; const currentPage = cleanSlug.join("/"); + const url = `${baseUrl}/${lang}/${currentPage}`; return { title: page.data.title, description: page.data.description, alternates: { + canonical: url, languages: { zh: `${baseUrl}/zh/${currentPage}`, en: `${baseUrl}/en/${currentPage}`, "x-default": `${baseUrl}/en/${currentPage}`, }, }, + openGraph: { + title: page.data.title, + description: page.data.description, + url, + siteName: "Project CVSA", + locale: lang === "zh" ? "zh_CN" : "en_US", + type: "article", + }, + twitter: { + card: "summary_large_image", + title: page.data.title, + description: page.data.description, + }, }; } diff --git a/apps/docs/app/not-found.tsx b/apps/docs/app/not-found.tsx index 8906e81..0e8d6f2 100644 --- a/apps/docs/app/not-found.tsx +++ b/apps/docs/app/not-found.tsx @@ -1,17 +1,31 @@ import "./global.css"; import { Inter } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; +import type { Metadata } from "next"; const inter = Inter({ subsets: ["latin"], }); +export const metadata: Metadata = { + title: "Not Found - Project CVSA", + description: "The page you are looking for does not exist.", + robots: { index: false, follow: false }, +}; + export default function NotFound() { return ( <html lang="en" className={inter.className}> - <body className="flex flex-col min-h-screen"> + <body className="flex flex-col min-h-screen items-center justify-center gap-4"> <UpdateLanguage /> - <h1>Not Found</h1> + <h1 className="text-4xl font-bold">404</h1> + <p className="text-fd-muted-foreground">Page not found</p> + <a + href="/en" + className="text-fd-primary underline underline-offset-4 hover:text-fd-primary/80" + > + Go to homepage + </a> </body> </html> ); From 7eb098b65177a52bc71afa07cd74bff9fae183ef Mon Sep 17 00:00:00 2001 From: 13812773784 <tc.aibeto@outlook.com> Date: Sat, 16 May 2026 23:11:25 +0800 Subject: [PATCH 26/37] docs(git-commit): add git commit message specification document add the full git commit message specification rules file to the project --- .trae/rules/git-commit-message.md | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .trae/rules/git-commit-message.md diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..87c0628 --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,52 @@ +--- +alwaysApply: true +scene: git_message +--- + +### 基本格式 + +每条提交信息应遵循以下结构(只允许使用英文): + +```text +<类型>(<作用域>): <描述> + +[正文] + +[脚注] +``` + +- **类型**:必填,表示本次提交的类别(见下文“类型列表”)。 +- **作用域**:可选,用括号包裹,指明本次提交影响的代码范围(如模块名、组件名等)。 +- **描述**:必填,是对变更的简短说明,使用祈使句、现在时态,首字母小写,末尾不加句号。 +- **正文**:可选,对变更动机及与之前行为的对比进行详细说明。 +- **脚注**:可选,主要用于关联 Issue 或标记破坏性变更(BREAKING CHANGE)。 + +### 示例 + +1. 新增一个用户登录功能: + + ```text + feat(auth): add user login endpoint + ``` + +2. 包含破坏性变更的提交(需在脚注中标记): + + ```text + feat(api): change response format for user details + + BREAKING CHANGE: The 'user' field in the response is now nested under 'data'. + ``` + +### 允许的类型列表 + +- `feat`:新增功能。 +- `fix`:修复 bug。 +- `update`: 对代码内容做出调整,而原先的代码不视为 bug(如修改前端按钮的文案) +- `docs`:仅修改文档。 +- `style`:修改代码格式(不影响代码运行的变动,如空格、分号缺失等)。 +- `refactor`:重构代码(既不是新增功能,也不是修改 bug 的代码变动)。 +- `perf`:优化性能。 +- `test`:增加测试或修改现有测试。 +- `ci`:修改 CI 配置文件或脚本(例如:Travis, Circle, BrowserStack, SauceLabs)。 +- `chore`:其他不修改源代码或测试文件的更改。 +- `revert`:撤销之前的提交。 From 1bca05c1108948a59db19a6c4ffc4aabd0a4e2c0 Mon Sep 17 00:00:00 2001 From: 13812773784 <tc.aibeto@outlook.com> Date: Sun, 17 May 2026 00:25:50 +0800 Subject: [PATCH 27/37] docs(website): update landing page metadata and remove hardcoded title - Add complete Next.js Metadata configurations to landing page layout components, including titles, descriptions, canonical URLs, multilingual adaptation, Open Graph and Twitter Card information. - Remove hardcoded title tags in HTML templates and adopt unified metadata configurations instead. - Revise the main text and footnote notes in the Git commit specification document to use English description requirements. --- .trae/rules/git-commit-message.md | 4 ++-- apps/docs/app/(landing)/layout.tsx | 33 +++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md index 87c0628..6536d5c 100644 --- a/.trae/rules/git-commit-message.md +++ b/.trae/rules/git-commit-message.md @@ -10,9 +10,9 @@ scene: git_message ```text <类型>(<作用域>): <描述> -[正文] +[正文(英文描述)] -[脚注] +[脚注(英文描述)] ``` - **类型**:必填,表示本次提交的类别(见下文“类型列表”)。 diff --git a/apps/docs/app/(landing)/layout.tsx b/apps/docs/app/(landing)/layout.tsx index c999df5..0b10235 100644 --- a/apps/docs/app/(landing)/layout.tsx +++ b/apps/docs/app/(landing)/layout.tsx @@ -1,15 +1,46 @@ import "./global.css"; import { Funnel_Sans } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; +import type { Metadata } from "next"; const inter = Funnel_Sans({ subsets: ["latin"], }); +const baseUrl = "https://docs.projectcvsa.com"; + +export const metadata: Metadata = { + title: "Project CVSA | Archive for a Better Future", + description: + "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", + alternates: { + canonical: baseUrl, + languages: { + en: `${baseUrl}/en`, + zh: `${baseUrl}/zh`, + "x-default": `${baseUrl}/en`, + }, + }, + openGraph: { + title: "Project CVSA | Archive for a Better Future", + description: + "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", + url: baseUrl, + siteName: "Project CVSA", + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Project CVSA | Archive for a Better Future", + description: + "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", + }, +}; + export default function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={inter.className} suppressHydrationWarning> - <title>Project CVSA | Archive for a Better Future {children} From ffe158a937470eaea0f4118dd5eb1bfd343d4b37 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Fri, 22 May 2026 22:57:38 +0800 Subject: [PATCH 28/37] feat(artistRole): CRUD API for artistRole (#60) - Add language field to ArtistRole Prisma model - Implement core module: DTOs, repository (with soft-delete), service (with outbox), DI container - Add CRUD handlers for artistRole - Add search service extending ISearchService with single-language indexing - Remove rate limiting from login handler - Refactor search interface and update search services for artist/singer/song --- apps/backend/src/handlers/auth/login.ts | 93 +++--- apps/backend/src/handlers/auth/signup.ts | 87 +++-- .../src/handlers/catalog/artistRole/create.ts | 33 ++ .../src/handlers/catalog/artistRole/delete.ts | 34 ++ .../src/handlers/catalog/artistRole/get.ts | 32 ++ .../src/handlers/catalog/artistRole/index.ts | 13 + .../src/handlers/catalog/artistRole/search.ts | 25 ++ .../src/handlers/catalog/artistRole/update.ts | 39 +++ apps/backend/src/handlers/index.ts | 1 + apps/backend/src/index.ts | 2 + apps/backend/src/middlewares/auth.ts | 1 + apps/backend/tests/artistRole.test.ts | 257 +++++++++++++++ bun.lock | 303 +++++++++--------- package.json | 4 +- packages/core/src/modules/auth/betterAuth.ts | 8 + .../modules/catalog/artistRole/container.ts | 17 + .../src/modules/catalog/artistRole/dto.ts | 27 ++ .../src/modules/catalog/artistRole/index.ts | 5 + .../artistRole/repository.interface.ts | 29 ++ .../modules/catalog/artistRole/repository.ts | 70 ++++ .../src/modules/catalog/artistRole/service.ts | 92 ++++++ packages/core/src/modules/catalog/index.ts | 1 + packages/core/src/outbox/processor.ts | 8 +- packages/core/src/outbox/queue.ts | 1 + packages/core/src/search/catalog/artist.ts | 89 +---- .../core/src/search/catalog/artistRole.ts | 33 ++ packages/core/src/search/catalog/index.ts | 1 + packages/core/src/search/catalog/singer.ts | 90 +----- packages/core/src/search/catalog/song.ts | 96 +----- packages/core/src/search/config.ts | 10 + packages/core/src/search/interface.ts | 97 +++++- .../integration/ArtistRoleRepository.test.ts | 127 ++++++++ .../core/tests/unit/ArtistRoleService.test.ts | 178 ++++++++++ .../tests/unit/ArtistSearchService.test.ts | 5 +- .../tests/unit/SingerSearchService.test.ts | 5 +- .../core/tests/unit/SongSearchService.test.ts | 1 + .../db/prisma/models/core/artistRole.prisma | 1 + packages/db/prisma/models/core/song.prisma | 3 + packages/embedding/package.json | 2 +- packages/embedding/src/embedding.ts | 4 +- scripts/import.ts | 195 +++++++++++ scripts/tsconfig.json | 16 + 42 files changed, 1629 insertions(+), 506 deletions(-) create mode 100644 apps/backend/src/handlers/catalog/artistRole/create.ts create mode 100644 apps/backend/src/handlers/catalog/artistRole/delete.ts create mode 100644 apps/backend/src/handlers/catalog/artistRole/get.ts create mode 100644 apps/backend/src/handlers/catalog/artistRole/index.ts create mode 100644 apps/backend/src/handlers/catalog/artistRole/search.ts create mode 100644 apps/backend/src/handlers/catalog/artistRole/update.ts create mode 100644 apps/backend/tests/artistRole.test.ts create mode 100644 packages/core/src/modules/catalog/artistRole/container.ts create mode 100644 packages/core/src/modules/catalog/artistRole/dto.ts create mode 100644 packages/core/src/modules/catalog/artistRole/index.ts create mode 100644 packages/core/src/modules/catalog/artistRole/repository.interface.ts create mode 100644 packages/core/src/modules/catalog/artistRole/repository.ts create mode 100644 packages/core/src/modules/catalog/artistRole/service.ts create mode 100644 packages/core/src/search/catalog/artistRole.ts create mode 100644 packages/core/tests/integration/ArtistRoleRepository.test.ts create mode 100644 packages/core/tests/unit/ArtistRoleService.test.ts create mode 100644 scripts/import.ts create mode 100644 scripts/tsconfig.json diff --git a/apps/backend/src/handlers/auth/login.ts b/apps/backend/src/handlers/auth/login.ts index b78ad55..4d7b7f1 100644 --- a/apps/backend/src/handlers/auth/login.ts +++ b/apps/backend/src/handlers/auth/login.ts @@ -1,7 +1,5 @@ import { Elysia } from "elysia"; import { ip } from "elysia-ip"; -import { rateLimit } from "elysia-rate-limit"; -import { RateLimitError } from "@common/error"; import { AppError, auth, @@ -16,60 +14,49 @@ import { traceTask } from "@/common/trace"; const DAY = 86400; -export const loginHandler = new Elysia() - .use(ip()) - .use( - rateLimit({ - scoping: "scoped", - max: 50, - duration: 5 * 60 * 1000, - generator: () => "", - errorResponse: new RateLimitError(), - }) - ) - .post( - "/session", - async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await traceTask("auth.signInEmail", async () => { - return await auth.api.signInEmail({ - body: { - email: body.email, - password: body.password, - }, - headers: toBetterAuthHeaders(headers), - }); +export const loginHandler = new Elysia().use(ip()).post( + "/session", + async ({ body, status, headers, cookie: { token: tokenCookie } }) => { + const { user, token } = await traceTask("auth.signInEmail", async () => { + return await auth.api.signInEmail({ + body: { + email: body.email, + password: body.password, + }, + headers: toBetterAuthHeaders(headers), }); + }); - if (!token) { - throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, { - cause: "Better Auth responded with no token", - }); - } + if (!token) { + throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, { + cause: "Better Auth responded with no token", + }); + } - tokenCookie.value = token; - tokenCookie.httpOnly = true; - tokenCookie.maxAge = 90 * DAY; - tokenCookie.secure = true; - tokenCookie.sameSite = "lax"; + tokenCookie.value = token; + tokenCookie.httpOnly = true; + tokenCookie.maxAge = 90 * DAY; + tokenCookie.secure = true; + tokenCookie.sameSite = "lax"; - const userInfo = betterAuthToLoginUserInfoDto(user, token); - const response = toLoginResponse(userInfo); + const userInfo = betterAuthToLoginUserInfoDto(user, token); + const response = toLoginResponse(userInfo); - return status(200, response); + return status(200, response); + }, + { + body: LoginRequestSchema, + detail: { + summary: "User Login", + description: + "Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.", }, - { - body: LoginRequestSchema, - detail: { - summary: "User Login", - description: - "Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.", - }, - response: { - 200: LoginResponseSchema, - 401: ErrorResponseSchema, - 422: ErrorResponseSchema, - 429: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } - ); + response: { + 200: LoginResponseSchema, + 401: ErrorResponseSchema, + 422: ErrorResponseSchema, + 429: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/handlers/auth/signup.ts b/apps/backend/src/handlers/auth/signup.ts index 7e8d05b..8a0a77d 100644 --- a/apps/backend/src/handlers/auth/signup.ts +++ b/apps/backend/src/handlers/auth/signup.ts @@ -1,6 +1,5 @@ import { Elysia } from "elysia"; import { ip } from "elysia-ip"; -import { rateLimit } from "elysia-rate-limit"; import { betterAuthToSignupUserInfoDto, SignupRequestSchema, @@ -10,64 +9,52 @@ import { ErrorResponseSchema, toBetterAuthHeaders, } from "@cvsa/core"; -import { RateLimitError } from "@common/error"; import { AppError } from "@cvsa/core"; import { auth } from "@cvsa/core"; import { traceTask } from "@/common/trace"; const DAY = 86400; -export const signupHandler = new Elysia() - .use(ip()) - .use( - rateLimit({ - scoping: "scoped", - max: 50, - duration: 5 * 60 * 1000, - generator: () => "", // global limit - errorResponse: new RateLimitError(), - }) - ) - .post( - "/user", - async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await traceTask("auth.signUpEmail", async () => { - return await auth.api.signUpEmail({ - body: signupRequestToBetterAuth(body), - headers: toBetterAuthHeaders(headers), - }); +export const signupHandler = new Elysia().use(ip()).post( + "/user", + async ({ body, status, headers, cookie: { token: tokenCookie } }) => { + const { user, token } = await traceTask("auth.signUpEmail", async () => { + return await auth.api.signUpEmail({ + body: signupRequestToBetterAuth(body), + headers: toBetterAuthHeaders(headers), }); + }); - if (!token) { - throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, { - cause: "Better Auth responded with no token", - }); - } + if (!token) { + throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, { + cause: "Better Auth responded with no token", + }); + } - tokenCookie.value = token; - tokenCookie.httpOnly = true; - tokenCookie.maxAge = 90 * DAY; - tokenCookie.secure = true; - tokenCookie.sameSite = "lax"; + tokenCookie.value = token; + tokenCookie.httpOnly = true; + tokenCookie.maxAge = 90 * DAY; + tokenCookie.secure = true; + tokenCookie.sameSite = "lax"; - const userInfo = betterAuthToSignupUserInfoDto(user, token); - const response = toSignUpResponse(userInfo); + const userInfo = betterAuthToSignupUserInfoDto(user, token); + const response = toSignUpResponse(userInfo); - return status(200, response); + return status(200, response); + }, + { + body: SignupRequestSchema, + detail: { + summary: "User Registration", + description: + "Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.", }, - { - body: SignupRequestSchema, - detail: { - summary: "User Registration", - description: - "Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.", - }, - response: { - 200: SignupResponseSchema, - 400: ErrorResponseSchema, - 422: ErrorResponseSchema, - 429: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } - ); + response: { + 200: SignupResponseSchema, + 400: ErrorResponseSchema, + 422: ErrorResponseSchema, + 429: ErrorResponseSchema, + 500: ErrorResponseSchema, + }, + } +); diff --git a/apps/backend/src/handlers/catalog/artistRole/create.ts b/apps/backend/src/handlers/catalog/artistRole/create.ts new file mode 100644 index 0000000..30eaf21 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/create.ts @@ -0,0 +1,33 @@ +import { Elysia } from "elysia"; +import { + CreateArtistRoleRequestSchema, + ErrorResponseSchema, + ArtistRoleResponseSchema, + artistRoleService, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistRoleCreateHandler = new Elysia({ name: "artistRoleCreateHandler" }) + .use(authMiddleware) + .post( + "/artist-role", + async ({ body, status }) => { + const role = await traceTask("artistRoleService.create", async () => { + return await artistRoleService.create(body); + }); + return status(201, role); + }, + { + body: CreateArtistRoleRequestSchema, + detail: { + summary: "Create Artist Role", + description: + "Create a new artist role entry in the catalog. Requires authentication.", + }, + response: { + 201: ArtistRoleResponseSchema, + 401: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/artistRole/delete.ts b/apps/backend/src/handlers/catalog/artistRole/delete.ts new file mode 100644 index 0000000..498ede0 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/delete.ts @@ -0,0 +1,34 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { ErrorResponseSchema, artistRoleService } from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistRoleDeleteHandler = new Elysia({ name: "artistRoleDeleteHandler" }) + .use(authMiddleware) + .delete( + "/artist-role/:id", + async ({ params, set }) => { + await traceTask("artistRoleService.delete", async () => { + return await artistRoleService.delete(params.id); + }); + set.status = 204; + return null; + }, + { + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Delete Artist Role", + description: + "Soft delete an artist role from the catalog. Requires authentication. The role record is marked as deleted but retained in the database for data integrity.", + }, + response: { + 204: z.null(), + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/catalog/artistRole/get.ts b/apps/backend/src/handlers/catalog/artistRole/get.ts new file mode 100644 index 0000000..7857ce2 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/get.ts @@ -0,0 +1,32 @@ +import { Elysia } from "elysia"; +import { + ErrorResponseSchema, + ArtistRoleDetailsResponseSchema, + artistRoleService, +} from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +export const artistRoleDetailsHandler = new Elysia().get( + "/artist-role/:id", + async ({ params, status }) => { + const role = await traceTask("artistRoleService.getDetails", async () => { + return await artistRoleService.getDetails(params.id); + }); + return status(200, role); + }, + { + detail: { + summary: "Artist Role Details", + description: "Retrieve detailed information about a specific artist role by its ID.", + }, + response: { + 200: ArtistRoleDetailsResponseSchema, + 400: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/artistRole/index.ts b/apps/backend/src/handlers/catalog/artistRole/index.ts new file mode 100644 index 0000000..1d01520 --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/index.ts @@ -0,0 +1,13 @@ +import { Elysia } from "elysia"; +import { artistRoleCreateHandler } from "./create"; +import { artistRoleUpdateHandler } from "./update"; +import { artistRoleDeleteHandler } from "./delete"; +import { artistRoleDetailsHandler } from "./get"; +import { artistRoleSearchHandler } from "./search"; + +export const artistRoleHandler = new Elysia({ name: "artistRoleHandler" }) + .use(artistRoleDetailsHandler) + .use(artistRoleCreateHandler) + .use(artistRoleUpdateHandler) + .use(artistRoleDeleteHandler) + .use(artistRoleSearchHandler); diff --git a/apps/backend/src/handlers/catalog/artistRole/search.ts b/apps/backend/src/handlers/catalog/artistRole/search.ts new file mode 100644 index 0000000..6542fcb --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/search.ts @@ -0,0 +1,25 @@ +import { Elysia } from "elysia"; +import { artistRoleSearchService } from "@cvsa/core"; +import z from "zod"; +import { traceTask } from "@/common/trace"; + +// TODO: add corresponding DTO and response schema +export const artistRoleSearchHandler = new Elysia().get( + "/artist-roles", + async ({ query, status }) => { + const result = await traceTask("artistRoleSearchService.search", async () => { + return await artistRoleSearchService.search(query.q || ""); + }); + return status(200, result); + }, + { + detail: { + summary: "Search for Artist Roles", + description: + "Full-text search across artist role names. Returns a list of matching roles ordered by relevance.", + }, + query: z.object({ + q: z.string().optional(), + }), + } +); diff --git a/apps/backend/src/handlers/catalog/artistRole/update.ts b/apps/backend/src/handlers/catalog/artistRole/update.ts new file mode 100644 index 0000000..759418f --- /dev/null +++ b/apps/backend/src/handlers/catalog/artistRole/update.ts @@ -0,0 +1,39 @@ +import { Elysia } from "elysia"; +import { z } from "zod"; +import { + UpdateArtistRoleRequestSchema, + ErrorResponseSchema, + artistRoleService, + ArtistRoleResponseSchema, +} from "@cvsa/core"; +import { authMiddleware } from "@/middlewares"; +import { traceTask } from "@/common/trace"; + +export const artistRoleUpdateHandler = new Elysia({ name: "artistRoleUpdateHandler" }) + .use(authMiddleware) + .patch( + "/artist-role/:id", + async ({ params, body, status }) => { + const role = await traceTask("artistRoleService.update", async () => { + return await artistRoleService.update(params.id, body); + }); + return status(200, role); + }, + { + body: UpdateArtistRoleRequestSchema, + params: z.object({ + id: z.coerce.number().int().positive(), + }), + detail: { + summary: "Update Artist Role", + description: + "Update metadata of an existing artist role identified by its ID. Requires authentication.", + }, + response: { + 200: ArtistRoleResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + 404: ErrorResponseSchema, + }, + } + ); diff --git a/apps/backend/src/handlers/index.ts b/apps/backend/src/handlers/index.ts index 4e3b6cd..95f13f5 100644 --- a/apps/backend/src/handlers/index.ts +++ b/apps/backend/src/handlers/index.ts @@ -2,5 +2,6 @@ export * from "./auth"; export * from "./catalog/song"; export * from "./catalog/engine"; export * from "./catalog/artist"; +export * from "./catalog/artistRole"; export * from "./catalog/singer"; export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index ae61139..53f93ac 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -7,6 +7,7 @@ import { songHandler, engineHandler, artistHandler, + artistRoleHandler, singerHandler, } from "@handlers/index"; import { errorHandler } from "./errorHandler"; @@ -64,6 +65,7 @@ export const app = new Elysia({ .use(songHandler) .use(engineHandler) .use(artistHandler) + .use(artistRoleHandler) .use(singerHandler) .use(devHandler) .listen(16412); diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts index ec714de..c35eb19 100644 --- a/apps/backend/src/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -1,5 +1,6 @@ import { Elysia } from "elysia"; import { AppError, auth, betterAuthUserToEntity, toBetterAuthHeaders } from "@cvsa/core"; +import { appLogger } from "@cvsa/logger"; export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( { as: "scoped" }, diff --git a/apps/backend/tests/artistRole.test.ts b/apps/backend/tests/artistRole.test.ts new file mode 100644 index 0000000..5ad0457 --- /dev/null +++ b/apps/backend/tests/artistRole.test.ts @@ -0,0 +1,257 @@ +import { describe, expect, test } from "bun:test"; +import { treaty } from "@elysiajs/eden"; +import { app } from "@/index"; +import { prisma } from "@cvsa/db"; + +const api = treaty(app); + +describe("Artist Role E2E Tests", () => { + async function getAuthToken() { + const signup = await api.v2.user.post({ + username: `${Math.random()}`, + password: "password123", + }); + return signup.data?.data.token; + } + + describe("GET /v2/artist-role/:id - Get Artist Role Details", () => { + test("should retrieve an artist role", async () => { + const role = await prisma.artistRole.create({ + data: { + name: "Producer", + localizedNames: { zh: "制作人", en: "Producer" }, + }, + }); + + const { data, status } = await api.v2["artist-role"]({ id: role.id }).get(); + + expect(status).toBe(200); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Producer", + localizedNames: { zh: "制作人", en: "Producer" }, + }); + }); + + test("should return 404 for nonexistent role", async () => { + const { error, status } = await api.v2["artist-role"]({ id: 99999 }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + + test("should not retrieve soft-deleted role", async () => { + const role = await prisma.artistRole.create({ + data: { + name: "Deleted Role", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2["artist-role"]({ id: role.id }).get(); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("POST /v2/artist-role - Create Artist Role", () => { + test("should create a role with authentication", async () => { + const token = await getAuthToken(); + + const payload = { + name: "Composer", + localizedNames: { zh: "作曲人", ja: "作曲家" }, + }; + + const { data, status } = await api.v2["artist-role"].post(payload, { + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Composer", + localizedNames: { zh: "作曲人", ja: "作曲家" }, + }); + }); + + test("should return 401 without authentication", async () => { + const payload = { + name: "Test Role", + }; + + const { error, status } = await api.v2["artist-role"].post(payload); + + expect(status).toBe(401); + expect(error?.value).toMatchObject({ + code: "UNAUTHORIZED", + }); + }); + + test("should create a role with minimal fields", async () => { + const token = await getAuthToken(); + + const { data, status } = await api.v2["artist-role"].post( + { name: "Illustrator" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(201); + expect(data).toMatchObject({ + id: expect.any(Number), + name: "Illustrator", + }); + }); + }); + + describe("PATCH /v2/artist-role/:id - Update Artist Role", () => { + test("should update a role with authentication", async () => { + const token = await getAuthToken(); + + const role = await prisma.artistRole.create({ + data: { + name: "Original Name", + }, + }); + + const { data, status } = await api.v2["artist-role"]({ id: role.id }).patch( + { + name: "Updated Name", + localizedNames: { zh: "更新名称" }, + }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(200); + expect(data?.name).toBe("Updated Name"); + expect(data?.localizedNames).toEqual({ zh: "更新名称" }); + }); + + test("should return 401 without authentication", async () => { + const role = await prisma.artistRole.create({ + data: { + name: "Test Role", + }, + }); + + const { status } = await api.v2["artist-role"]({ id: role.id }).patch({ + name: "Updated Name", + }); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent role", async () => { + const token = await getAuthToken(); + + const { error, status } = await api.v2["artist-role"]({ id: 999999 }).patch( + { name: "Updated Name" }, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); + + describe("DELETE /v2/artist-role/:id - Delete Artist Role", () => { + test("should soft delete a role with authentication", async () => { + const token = await getAuthToken(); + + const role = await prisma.artistRole.create({ + data: { + name: "Role to Delete", + }, + }); + + const { status } = await api.v2["artist-role"]({ id: role.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(204); + + const deletedRole = await prisma.artistRole.findUnique({ + where: { id: role.id }, + }); + expect(deletedRole?.deletedAt).not.toBeNull(); + }); + + test("should return 401 without authentication", async () => { + const role = await prisma.artistRole.create({ + data: { + name: "Test Role", + }, + }); + + const { status } = await api.v2["artist-role"]({ id: role.id }).delete({}, {}); + + expect(status).toBe(401); + }); + + test("should return 404 for non-existent role", async () => { + const token = await getAuthToken(); + + const { status } = await api.v2["artist-role"]({ id: 999999 }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + }); + + test("should return 404 when trying to delete already deleted role", async () => { + const token = await getAuthToken(); + + const role = await prisma.artistRole.create({ + data: { + name: "Already Deleted", + deletedAt: new Date(), + }, + }); + + const { error, status } = await api.v2["artist-role"]({ id: role.id }).delete( + {}, + { + headers: { + authorization: `Bearer ${token}`, + }, + } + ); + + expect(status).toBe(404); + expect(error?.value).toMatchObject({ + code: "NOT_FOUND", + }); + }); + }); +}); diff --git a/bun.lock b/bun.lock index 31ca553..c2ea5e8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { "name": "cvsa", @@ -7,7 +8,7 @@ "@biomejs/biome": "^2.4.10", "@types/bun": "^1.3.10", "turbo": "^2.8.18", - "typescript": "^6.0.2", + "typescript": "^6.0.3", }, }, "apps/backend": { @@ -93,7 +94,7 @@ "@huggingface/hub": "^2.11.0", "@huggingface/transformers": "^4.0.1", "elysia": "latest", - "onnxruntime-node": "^1.24.3", + "onnxruntime-node": "1.24.3", "zod": "^4.3.6", }, "devDependencies": { @@ -151,48 +152,44 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@better-auth/core": ["@better-auth/core@1.5.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw=="], + "@better-auth/core": ["@better-auth/core@1.6.11", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-4jpkETIGZOHCf7BK4jnu22fdN6jjomH0/HhEzkaWy3+Eppi5PYlHTF/460jrTmA3Xc+Vqwp9t282ymHiEPypGw=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "kysely": "^0.28.17" }, "optionalPeers": ["kysely"] }, "sha512-/g8M9RfIjdcZDnbstSUvQiINkvdNlCeZr248zwqx2/PVksQI1MhQofbzUn3RnQnbPKp0EPwpX/dR3oudRFenUg=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0" } }, "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0" } }, "sha512-hpdfw0BBf8MuzLkIdmbcUZICbY9r/bhLO2RxSnkzT5+/O+0I0u2I8+m0YUP7vNllP/ZCKASHOYgXPLO75Z0f9Q=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-3Tor8rSv8vSEIMEaV2PFpPEuVhqc1gNoZ6eGvoh3LwExXXuj8madew6ob+H1pH7Aphn3Ar5PQ08AguT8TbwFAA=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.6", "", { "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-Pw+7q7zTp+VSci1V+CYMvuxIbAeVMZLe4lRo46LJoAKMHfjFl5T/ycsyFvWs/DkWC7n9gZZzRDEbHp0I5FiKKw=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.6", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.6" } }, "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-hsjDHc8MZbm6/AHeNdtywrWedXevnBjmdvnHTcZub+rTVjOv+Td0roI8USKuC6uUibmrl//2rJfVCsGbopihNA=="], - "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], + "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], - "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], - - "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], - "@cvsa/backend": ["@cvsa/backend@workspace:apps/backend"], "@cvsa/core": ["@cvsa/core@workspace:packages/core"], @@ -219,31 +216,35 @@ "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], - "@elysiajs/openapi": ["@elysiajs/openapi@1.4.14", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-kWmJWdvP8/LwHwAJXSpz6xFfYUoyUyEPRimEYABuDU1rOnS27Da1u9T2jyU7frOopxKWV/wDfDxMP8z2xdCPJw=="], + "@elysiajs/openapi": ["@elysiajs/openapi@1.4.15", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-HDtGIrWfFJk6UJS7qbgRmjifbSqnnRGDM3CsU/GVJkxLbEVEkNuQDf+Quh9fGbytSrJ8g4+tX9eVjshYhCH29w=="], "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.11", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-GGggB40rXithCA9nSjSc25TFkBPeEEW3rD8me/TrRq4TejzWFJn5zEKdRMYWSpYBeiUsraCpQxu94K2ASz+UnA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], "@fanee/runtime-node": ["@fanee/runtime-node@0.2.1", "", { "dependencies": { "messageformat": "^4.0.0" } }, "sha512-txiKblLxjsO49SYeZDb9CClBp3EC7lp0kn7Zfm1CDT47nbrxIvUqupH/DOQbE0gjPIyv8MOkXZmjmBTORvxd7w=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="], - "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - "@huggingface/hub": ["@huggingface/hub@2.11.0", "", { "dependencies": { "@huggingface/tasks": "^0.19.90" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ=="], + "@huggingface/blake3-jit": ["@huggingface/blake3-jit@0.0.2", "", {}, "sha512-Bq7B5qabyjrJfhBsl85Jd2QBtf+HzRD7h7A9GfN2lzrrsABhOa5evVPgzoCTxR7Ub0QFj7YDK1YkYRWBU25+2w=="], + + "@huggingface/hub": ["@huggingface/hub@2.12.0", "", { "dependencies": { "@huggingface/tasks": "^0.20.25", "@huggingface/xetchunk-wasm": "^0.0.6" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-PU+CBtOVsg6e35LkHBe5WjkIxwEhGkoh93ZLIi/J/zgvv4JhLmfwtQ8+t/HodibBzIIloC1jvaAMkJqDJaml1g=="], - "@huggingface/jinja": ["@huggingface/jinja@0.5.6", "", {}, "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA=="], + "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], - "@huggingface/tasks": ["@huggingface/tasks@0.19.90", "", {}, "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA=="], + "@huggingface/tasks": ["@huggingface/tasks@0.20.25", "", {}, "sha512-id1Yp40dLFh2Hqy6PFN+9+PEXyKCB13hq6JSe0AkHfq3XVGjQGEO8C0UFVFUlHWCzB7FgI+u/hXn6aeRs/SE9w=="], "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], - "@huggingface/transformers": ["@huggingface/transformers@4.0.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", "sharp": "^0.34.5" } }, "sha512-tAQYEy+cnW0ku/NxBSjFXCymi+DZa1/JkoGf4McxjzO36CZZIL/J4TF6X7i/tzs75yTjshUDgsvSz03s2xym2A=="], + "@huggingface/transformers": ["@huggingface/transformers@4.2.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", "sharp": "^0.34.5" } }, "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ=="], + + "@huggingface/xetchunk-wasm": ["@huggingface/xetchunk-wasm@0.0.6", "", { "dependencies": { "@huggingface/blake3-jit": "0.0.2", "gearhash-jit": "1.0.2" } }, "sha512-LoYPl7jvUvOnkysrhMjAeBzK5mVe2GFu8O3IsZb1w7l7v+NqjDsyRduwct3yraPAGmzTxxdb/2aIORL98Y949g=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -313,9 +314,9 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], @@ -323,7 +324,7 @@ "@opentelemetry/configuration": ["@opentelemetry/configuration@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-Q+awuEwxhETwIAXuxHvIY5ZMEP0ZqvxLTi9kclrkyVJppEUXYL3Bhiw3jYrxdHYMh0Y0tVInQH9FEZ1aMinvLA=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], @@ -361,7 +362,7 @@ "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-kKFMxBcjBZAC1vBch5mtZ/dJQvcAEKWga+c+q5iGgRLPIE6Mc649zEwMaCIQCzalziMJQiyUadFYMHmELB7AFw=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], @@ -371,39 +372,39 @@ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.8.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.8.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg=="], - "@prisma/client": ["@prisma/client@7.6.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.6.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-7Pe/1ayh3GgWPEg4mmT4ax77LJ1wC+XlnIFvQ94bLP2DsUnOpnruQQR3Jw7r+Frthk94QqDNxo3FjSg8h9PXeQ=="], + "@prisma/client": ["@prisma/client@7.8.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.8.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw=="], - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.6.0", "", {}, "sha512-fD7jlqubsZvVODKvsp9lOpXVecx2aWGxC2l35Ioz2t+teUJ5CfR0SAMsi7UkU1VvaZmmm+DS6BdujF622nY7tQ=="], + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.8.0", "", {}, "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw=="], - "@prisma/config": ["@prisma/config@7.6.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-MuAz1MK4PeG5/03YzfzX3CnFVHQ6qePGwUpQRzPzX5tT0ffJ3Tzi9zJZbBc+VzEGFCM8ghW/gTVDR85Syjt+Yw=="], + "@prisma/config": ["@prisma/config@7.8.0", "", { "dependencies": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw=="], - "@prisma/debug": ["@prisma/debug@7.6.0", "", {}, "sha512-LpHr3qos4lQZ6sxwjStf59YBht7m9/QF7NSQsMH6qGENWZu2w3UkQUGn1h5iRkDjnWRj3VHykOu9qFhps4ADvA=="], + "@prisma/debug": ["@prisma/debug@7.8.0", "", {}, "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA=="], "@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="], - "@prisma/dmmf": ["@prisma/dmmf@7.6.0", "", {}, "sha512-OZFEOEziUnJJMx2vu3twM5jEIsEyCRDPXq2Y/2XMBNPhnscKvbDWS3DAYyWmH8jqkDn4VqyE1UCvEFt30Pqbww=="], + "@prisma/dmmf": ["@prisma/dmmf@7.8.0", "", {}, "sha512-7xzcSFWO6J+dFUgIX7jL7QqUhEDfaa8GSZGsjjHyZct1Su+6KrvMl3S2+fnRkuKUIoTPg3Mj02oZuUdaNSfsaw=="], - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-D8j3p0RnhLuufMaRLX6QqtGgPC5Ao3l5oFP6Q5AL0rTHi4vna+NzGEipwCsfvcSvaGFCbsH3lsTMbb4WvY+ovA=="], + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A=="], - "@prisma/engines": ["@prisma/engines@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/fetch-engine": "7.6.0", "@prisma/get-platform": "7.6.0" } }, "sha512-Sn5edRzhHqgRV2M+A0eIbY442B4mReWWf3pKs/LKreYgW7oa/up8JtK/s4iv/EQA097cyboZ08mmkpbLp+tZ3w=="], + "@prisma/engines": ["@prisma/engines@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/fetch-engine": "7.8.0", "@prisma/get-platform": "7.8.0" } }, "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw=="], - "@prisma/engines-version": ["@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "", {}, "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw=="], + "@prisma/engines-version": ["@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "", {}, "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/get-platform": "7.6.0" } }, "sha512-N575Ni95c3FkduWY/eKTHqNYgNbceZ1tQaSknVtJjpKmiiBXmniESn/GTxsDvICC4ZeiNrXxioGInzQrCdx16w=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/get-platform": "7.8.0" } }, "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ=="], - "@prisma/generator": ["@prisma/generator@7.6.0", "", {}, "sha512-F09ZN/QVlKuvoAKEnHZB3LBbooKQTrio7NrKhI7tf0C9XZuCoALP/F7gADpTayXDmPaII1Klit3SJmJsfantEw=="], + "@prisma/generator": ["@prisma/generator@7.8.0", "", {}, "sha512-KHGB0b8/9pNWyiK9EPJNE2/v1bMtqJgJldqjNNVvoE4uOhNSSWTmhHhPVfRsiuOVybzHCdCUQ/gdidCbpYAD5w=="], - "@prisma/generator-helper": ["@prisma/generator-helper@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0", "@prisma/dmmf": "7.6.0", "@prisma/generator": "7.6.0" } }, "sha512-Wnb7NypN2581AXsaDdfGM6Guvfg6ahs0ooWHhFwD+/mrE3rI122cNRWMV9W1hqyt5OITs+3TV2M6GZZ16NcwjQ=="], + "@prisma/generator-helper": ["@prisma/generator-helper@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/dmmf": "7.8.0", "@prisma/generator": "7.8.0" } }, "sha512-i+2Gad6D/0dS0YHKFdYX3M8KYN1gwNkET813WXKfW2HeWmgipmSJsNSzOA44kTM+Rx6Dev3yBQwx5sZXXdtgtQ=="], "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], @@ -417,21 +418,21 @@ "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -459,25 +460,25 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.14", "", { "os": "linux", "cpu": "x64" }, "sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.14", "", { "os": "win32", "cpu": "x64" }, "sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], @@ -487,7 +488,7 @@ "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -499,27 +500,25 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "better-auth": ["better-auth@1.5.6", "", { "dependencies": { "@better-auth/core": "1.5.6", "@better-auth/drizzle-adapter": "1.5.6", "@better-auth/kysely-adapter": "1.5.6", "@better-auth/memory-adapter": "1.5.6", "@better-auth/mongo-adapter": "1.5.6", "@better-auth/prisma-adapter": "1.5.6", "@better-auth/telemetry": "1.5.6", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.12", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ=="], + "better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="], - "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], - "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], + "better-result": ["better-result@2.9.2", "", {}, "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q=="], "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "bullmq": ["bullmq@5.75.2", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-5GDO2L5OfzogDxzJCeT7icYdI41vQZ5Wuw2z4EqpfPc1m4/gxyg0sEATULGPdR1sMw0kwCPqbehiIPseXxBX9A=="], + "bullmq": ["bullmq@5.77.0", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "2.0.1", "node-abort-controller": "3.1.1", "semver": "7.8.0", "tslib": "2.8.1" }, "peerDependencies": { "redis": ">=5.0.0" }, "optionalPeers": ["redis"] }, "sha512-J8CBvBgov4hqj/5CipTLtgfINvNRDeTl9p2Et/Dah/KBY2ERcnMT1m3ZxW4R1QC68ZX6yQk5nONtslpbbTs2gw=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], @@ -537,8 +536,6 @@ "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], @@ -559,7 +556,7 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -569,15 +566,15 @@ "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], - "elysia-ip": ["elysia-ip@1.0.10", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-xmCxPOl4266sq6CLk5d82P3BZOatG9z0gMP473cYEnORssuopbEI8GAwpOhiaz69X76AOrkYgvCdLkqMJC49dQ=="], + "elysia-ip": ["elysia-ip@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-c4wgDzKRTjkDDZqBS3U8t70FBXFIYS288viIX+WsePC3CH0v0SLnJJfqADYdCDFg+/NP2X/dH291AeVdFNEfWw=="], - "elysia-rate-limit": ["elysia-rate-limit@4.5.1", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-36u0BBMWV8Ae5f4q8k19Yuq4UusjbFTstZ7Ej1RU6m/Xc94RZHfai8kn5lOsC+LEduZJr4JdQlZu+jhuSxQfyw=="], + "elysia-rate-limit": ["elysia-rate-limit@4.6.2", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-3axf0dl9PECqg+Duo+qfiI/RWyYnl63osuVgaI4DnXJjLs7PxNzJINfnKIV1Tyg0mqONO1jzdAjwDrJ3HEmuug=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -603,7 +600,7 @@ "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], - "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], + "fast-copy": ["fast-copy@4.0.3", "", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -611,9 +608,9 @@ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "file-type": ["file-type@22.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw=="], + "file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="], "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], @@ -621,13 +618,15 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gearhash-jit": ["gearhash-jit@1.0.2", "", {}, "sha512-UhzJL4KXSdqAKepy/tZwmi2Rcy0YMmtiC4DQS4SURCuIWdh8ECZtnXK2ePRMLigfB61hRKdLK/Vgg2bSw73izQ=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], - "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], @@ -645,11 +644,11 @@ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="], + "hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="], "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], @@ -661,7 +660,7 @@ "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -669,9 +668,9 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -679,7 +678,7 @@ "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="], + "kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -707,7 +706,7 @@ "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + "msgpackr": ["msgpackr@2.0.1", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], @@ -715,22 +714,18 @@ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], + "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], - "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -743,7 +738,7 @@ "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], - "onnxruntime-web": ["onnxruntime-web@1.25.0-dev.20260327-722743c0e2", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-8PXdZy4Ekhg10CLg+cFFt39b4tFDGMRJB6lGjnQL6eA+2boUQYDymZ0gtxiS+H6oIWoCjQp/ziyirvFbaFKfiw=="], + "onnxruntime-web": ["onnxruntime-web@1.26.0-dev.20260416-b7804b056c", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -753,26 +748,24 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], - "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], + "pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], - "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], - "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + "pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], - "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + "pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], @@ -781,7 +774,7 @@ "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], @@ -795,7 +788,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prisma": ["prisma@7.6.0", "", { "dependencies": { "@prisma/config": "7.6.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.6.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-OKJIPT81K3+F+AayIkY/Y3mkF2NWoFh7lZApaaqPYy7EHILKdO0VsmGkP+hDKYTySHsFSyLWXm/JgcR1B8fY1Q=="], + "prisma": ["prisma@7.8.0", "", { "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw=="], "prisma-json-types-generator": ["prisma-json-types-generator@4.1.1", "", { "dependencies": { "@prisma/generator-helper": "^7.2.0", "semver": "^7.7.3", "try": "^1.0.3", "tslib": "^2.8.1" }, "peerDependencies": { "@prisma/client": "^7.2.0", "prisma": "^7.2.0", "typescript": "^5.9.3" }, "bin": { "prisma-json-types-generator": "index.js" } }, "sha512-oz0MYu1r2Jqu3qOGCEGulz6RjPsECVjyS3ZAz3HEmz0jkKbVkZEwQyksNJm0X5utV1TZCmaG/98EPExN7S22xw=="], @@ -803,7 +796,7 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "protobufjs": ["protobufjs@7.6.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -811,13 +804,13 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], @@ -825,7 +818,7 @@ "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], - "remeda": ["remeda@2.33.7", "", {}, "sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w=="], + "remeda": ["remeda@2.34.1", "", {}, "sha512-k5iIF3lHm2NQ+2bNGDvZTD5jZl/JZkCS6AQOfGjYBd7V4rbb3K5whHvab0/O7CqPI43vYzbnIVCQXQJqmOyI6w=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -833,7 +826,7 @@ "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], @@ -849,7 +842,7 @@ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -869,8 +862,6 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -893,9 +884,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], - - "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], @@ -903,17 +892,15 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="], + "turbo": ["turbo@2.9.14", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.14", "@turbo/darwin-arm64": "2.9.14", "@turbo/linux-64": "2.9.14", "@turbo/linux-arm64": "2.9.14", "@turbo/windows-64": "2.9.14", "@turbo/windows-arm64": "2.9.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw=="], "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], @@ -927,7 +914,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -935,37 +922,61 @@ "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@cvsa/core/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@elysiajs/opentelemetry/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], - "@cvsa/env/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@cvsa/logger/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@cvsa/observability/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@elysiajs/opentelemetry/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], "@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], + "@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], - "@prisma/dev/remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.6.0", "", { "dependencies": { "@prisma/debug": "7.6.0" } }, "sha512-ohZDwXvtmnbzOcutR2D13lDWpZP1wQjmPyztmt0AwXLzQI7q95EE7NYCvS+M6N6SivT+BM0NOqLmTH3wms4L3A=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], - "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], - "@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "@prisma/dev/remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], - "ioredis/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], - "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + + "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], @@ -975,13 +986,7 @@ "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "@cvsa/core/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], - - "@cvsa/env/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], - - "@cvsa/logger/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], - - "@cvsa/observability/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], @@ -1023,14 +1028,12 @@ "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="], - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@3.0.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg=="], + "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@3.0.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA=="], "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "ioredis/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], diff --git a/package.json b/package.json index 94e7b5e..e32db53 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@biomejs/biome": "^2.4.10", "@types/bun": "^1.3.10", "turbo": "^2.8.18", - "typescript": "^6.0.2" + "typescript": "^6.0.3" }, - "packageManager": "bun@1.3.11" + "packageManager": "bun@1.3.12" } diff --git a/packages/core/src/modules/auth/betterAuth.ts b/packages/core/src/modules/auth/betterAuth.ts index 18f303b..5334c89 100644 --- a/packages/core/src/modules/auth/betterAuth.ts +++ b/packages/core/src/modules/auth/betterAuth.ts @@ -4,6 +4,7 @@ import { prisma } from "@cvsa/db"; import { getRandomId } from "@cvsa/core/internal"; import { username } from "better-auth/plugins"; import { bearer } from "better-auth/plugins"; +import { appLogger } from "@cvsa/logger"; const DAY = 24 * 60 * 60; const HOUR = 60 * 60; @@ -52,6 +53,13 @@ export const auth = betterAuth({ }), bearer(), ], + logger: { + log: (level, message, ...args) => { + appLogger[level](message, { + metadata: args, + }); + }, + }, }); export { APIError as BetterAuthAPIError }; diff --git a/packages/core/src/modules/catalog/artistRole/container.ts b/packages/core/src/modules/catalog/artistRole/container.ts new file mode 100644 index 0000000..871702e --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/container.ts @@ -0,0 +1,17 @@ +import { prisma } from "@cvsa/db"; +import { ArtistRoleRepository } from "./repository"; +import { ArtistRoleService } from "./service"; +import { ArtistRoleSearchService, searchManager } from "../../../search"; +import { treaty } from "@elysiajs/eden"; +import type { EmbeddingApp } from "@cvsa/embedding"; +import { outboxService } from "../../outbox/container"; + +const embeddingManager = treaty("localhost:14900"); + +export const artistRoleRepository = new ArtistRoleRepository(prisma); +export const artistRoleSearchService = new ArtistRoleSearchService( + artistRoleRepository, + searchManager, + embeddingManager +); +export const artistRoleService = new ArtistRoleService(artistRoleRepository, outboxService); diff --git a/packages/core/src/modules/catalog/artistRole/dto.ts b/packages/core/src/modules/catalog/artistRole/dto.ts new file mode 100644 index 0000000..9fc45b4 --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/dto.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; +import { ArtistRoleSchema, type Serialized } from "@cvsa/db"; + +export type ArtistRoleId = number; + +export const CreateArtistRoleRequestSchema = z.object({ + name: z.string(), + language: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), +}); + +export const UpdateArtistRoleRequestSchema = z.object({ + name: z.string().optional(), + language: z.string().optional(), + localizedNames: z.record(z.string(), z.string()).optional(), +}); + +export const ArtistRoleResponseSchema = ArtistRoleSchema.omit({ deletedAt: true }); + +export const ArtistRoleDetailsResponseSchema = ArtistRoleResponseSchema; + +export type ArtistRoleResponseDto = Serialized>; +export type ArtistRoleDetailsResponseDto = Serialized< + z.infer +>; +export type CreateArtistRoleRequestDto = Serialized>; +export type UpdateArtistRoleRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/artistRole/index.ts b/packages/core/src/modules/catalog/artistRole/index.ts new file mode 100644 index 0000000..132cf0c --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/index.ts @@ -0,0 +1,5 @@ +export * from "./dto"; +export * from "./repository"; +export * from "./service"; +export * from "./repository.interface"; +export * from "./container"; diff --git a/packages/core/src/modules/catalog/artistRole/repository.interface.ts b/packages/core/src/modules/catalog/artistRole/repository.interface.ts new file mode 100644 index 0000000..2d75397 --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/repository.interface.ts @@ -0,0 +1,29 @@ +import type { TxClient } from "@cvsa/db"; +import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; +import type { + CreateArtistRoleRequestDto, + ArtistRoleId, + UpdateArtistRoleRequestDto, + ArtistRoleResponseDto, + ArtistRoleDetailsResponseDto, +} from "./dto"; + +export abstract class IArtistRoleRepository + implements IRepositoryWithGetDetails +{ + abstract getById(id: ArtistRoleId, tx?: TxClient): Promise; + abstract getDetailsById( + id: ArtistRoleId, + tx?: TxClient + ): Promise; + abstract create( + input: CreateArtistRoleRequestDto, + tx?: TxClient + ): Promise; + abstract update( + id: ArtistRoleId, + input: UpdateArtistRoleRequestDto, + tx?: TxClient + ): Promise; + abstract softDelete(id: ArtistRoleId, tx?: TxClient): Promise; +} diff --git a/packages/core/src/modules/catalog/artistRole/repository.ts b/packages/core/src/modules/catalog/artistRole/repository.ts new file mode 100644 index 0000000..b3b6998 --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/repository.ts @@ -0,0 +1,70 @@ +import type { PrismaClient } from "@cvsa/db"; +import type { TxClient } from "@cvsa/db"; +import { BaseRepository } from "../../../utils/BaseRepository"; +import type { + CreateArtistRoleRequestDto, + ArtistRoleId, + UpdateArtistRoleRequestDto, + ArtistRoleDetailsResponseDto, +} from "./dto"; +import type { IArtistRoleRepository } from "./repository.interface"; + +export class ArtistRoleRepository extends BaseRepository implements IArtistRoleRepository { + constructor(private readonly prisma: PrismaClient) { + super(); + } + + async getById(id: ArtistRoleId, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.artistRole.getById", () => + client.artistRole.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async getDetailsById( + id: ArtistRoleId, + tx?: TxClient + ): Promise { + const client = tx ?? this.prisma; + return this.query("db.artistRole.getDetailsById", () => + client.artistRole.findFirst({ + where: { id, deletedAt: null }, + omit: { deletedAt: true }, + }) + ); + } + + async create(input: CreateArtistRoleRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.artistRole.create", () => + client.artistRole.create({ + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async update(id: ArtistRoleId, input: UpdateArtistRoleRequestDto, tx?: TxClient) { + const client = tx ?? this.prisma; + return this.query("db.artistRole.update", () => + client.artistRole.update({ + where: { id }, + data: input, + omit: { deletedAt: true }, + }) + ); + } + + async softDelete(id: ArtistRoleId, tx?: TxClient) { + const client = tx ?? this.prisma; + await this.query("db.artistRole.softDelete", () => + client.artistRole.update({ + where: { id }, + data: { deletedAt: new Date() }, + }) + ); + } +} diff --git a/packages/core/src/modules/catalog/artistRole/service.ts b/packages/core/src/modules/catalog/artistRole/service.ts new file mode 100644 index 0000000..67c031f --- /dev/null +++ b/packages/core/src/modules/catalog/artistRole/service.ts @@ -0,0 +1,92 @@ +import type { OutboxService } from "../../outbox/service"; +import { AppError } from "../../../error/AppError"; +import type { IServiceWithGetDetails } from "../../../types/service"; +import { prisma } from "@cvsa/db"; +import type { + ArtistRoleDetailsResponseDto, + ArtistRoleId, + CreateArtistRoleRequestDto, + UpdateArtistRoleRequestDto, + ArtistRoleResponseDto, +} from "./dto"; +import type { IArtistRoleRepository } from "./repository.interface"; + +export class ArtistRoleService implements IServiceWithGetDetails { + constructor( + private readonly repository: IArtistRoleRepository, + private readonly outbox: OutboxService + ) {} + + async getDetails(id: ArtistRoleId) { + const result = await this.repository.getDetailsById(id); + if (result === null) { + throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); + } + return result; + } + + async create(input: CreateArtistRoleRequestDto): Promise { + const { role, entry } = await prisma.$transaction(async (tx) => { + const role = await this.repository.create(input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "artistRole", + aggregateId: role.id, + eventType: "artistRole.created", + }, + tx + ); + return { role, entry }; + }); + + await this.outbox.enqueue(entry); + return role; + } + + async update( + id: ArtistRoleId, + input: UpdateArtistRoleRequestDto + ): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); + } + + const { role, entry } = await prisma.$transaction(async (tx) => { + const role = await this.repository.update(id, input, tx); + const entry = await this.outbox.createEntry( + { + aggregateType: "artistRole", + aggregateId: id, + eventType: "artistRole.updated", + }, + tx + ); + return { role, entry }; + }); + + await this.outbox.enqueue(entry); + return role; + } + + async delete(id: ArtistRoleId): Promise { + const existing = await this.repository.getById(id); + if (existing === null) { + throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); + } + + const entry = await prisma.$transaction(async (tx) => { + await this.repository.softDelete(id, tx); + return await this.outbox.createEntry( + { + aggregateType: "artistRole", + aggregateId: id, + eventType: "artistRole.deleted", + }, + tx + ); + }); + + await this.outbox.enqueue(entry); + } +} diff --git a/packages/core/src/modules/catalog/index.ts b/packages/core/src/modules/catalog/index.ts index 751a0f3..1681e44 100644 --- a/packages/core/src/modules/catalog/index.ts +++ b/packages/core/src/modules/catalog/index.ts @@ -1,4 +1,5 @@ export * from "./song"; export * from "./engine"; export * from "./artist"; +export * from "./artistRole"; export * from "./singer"; diff --git a/packages/core/src/outbox/processor.ts b/packages/core/src/outbox/processor.ts index 70ad4de..e2f772f 100644 --- a/packages/core/src/outbox/processor.ts +++ b/packages/core/src/outbox/processor.ts @@ -1,6 +1,11 @@ import type { Job } from "bullmq"; import type { OutboxEntryDto, OutboxService } from "@cvsa/core/internal"; -import { songSearchService, artistSearchService, singerSearchService } from "@cvsa/core/internal"; +import { + songSearchService, + artistSearchService, + singerSearchService, + artistRoleSearchService, +} from "@cvsa/core/internal"; import { outboxService } from "../modules/outbox/container"; import { appLogger } from "@cvsa/logger"; @@ -45,5 +50,6 @@ export const processOutboxEntry = createOutboxProcessor({ song: songSearchService, artist: artistSearchService, singer: singerSearchService, + artistRole: artistRoleSearchService, }, }); diff --git a/packages/core/src/outbox/queue.ts b/packages/core/src/outbox/queue.ts index 7922aac..9596e5a 100644 --- a/packages/core/src/outbox/queue.ts +++ b/packages/core/src/outbox/queue.ts @@ -10,6 +10,7 @@ let connection: IORedis | undefined; function getConnection(): IORedis { if (!connection) { + appLogger.debug(`Connecting to redis at ${env.REDIS_URL}`); connection = new IORedis(env.REDIS_URL, { maxRetriesPerRequest: null, }); diff --git a/packages/core/src/search/catalog/artist.ts b/packages/core/src/search/catalog/artist.ts index edc5e7d..d2e90d1 100644 --- a/packages/core/src/search/catalog/artist.ts +++ b/packages/core/src/search/catalog/artist.ts @@ -1,7 +1,5 @@ import { ISearchService } from "../interface"; -import { unique, keys } from "remeda"; import type { ArtistDetailsResponseDto } from "../../modules"; -import { appLogger } from "@cvsa/logger"; export interface ArtistSearchIndex { id: number; @@ -13,93 +11,30 @@ export interface ArtistSearchIndex { }; } -export class ArtistSearchService extends ISearchService { - private async getDocument( +export class ArtistSearchService extends ISearchService< + ArtistDetailsResponseDto, + ArtistSearchIndex +> { + protected readonly entityType = "artist"; + + protected async getDocument( artist: ArtistDetailsResponseDto, language: string ): Promise { - const getDesc = () => { - if (language === artist.language) return artist.description; - return artist.localizedDescriptions?.[language]; - }; - const getName = () => { - if (language === artist.language) return artist.name; - return artist.localizedNames?.[language]; - }; - const vectors = await this.embeddingManager.embeddings.post({ texts: [ - `Name: ${getName() ?? ""} -Description: ${getDesc() ?? ""} + `Name: ${this.getName(artist, language) ?? ""} +Description: ${this.getDescription(artist, language) ?? ""} Name Aliases: ${artist.aliases.join(", ")} `, ], }); return { id: artist.id, - name: getName() ?? undefined, + name: this.getName(artist, language) ?? undefined, aliases: artist.aliases, - description: getDesc() ?? undefined, - _vectors: vectors.data?.embeddings[0] - ? { - "potion-multilingual-128M": vectors.data?.embeddings[0], - } - : { - "potion-multilingual-128M": null, - }, + description: this.getDescription(artist, language) ?? undefined, + _vectors: this.buildVectors(vectors.data?.embeddings[0]), }; } - - public async sync(id: number) { - if (!this.searchManager) { - appLogger.warn("Search service not available"); - return; - } - const artist = await this.repository.getDetailsById(id); - - if (!artist) { - const indexesToBeDeleted = - await this.searchManager.getLocalizedIndexesOfEntity("artist"); - for (const index of indexesToBeDeleted) { - const adminIndex = await this.searchManager.getAdminIndex(index); - const task = await adminIndex.deleteDocument(id); - await this.searchManager.waitForTask(task.taskUid); - } - return; - } - - const languages = unique([ - ...keys(artist.localizedNames ?? []), - ...keys(artist.localizedDescriptions ?? []), - artist.language, - ]); - for (const language of languages) { - const indexUid = `artist_${language}`; - const index = await this.searchManager.getAdminIndex(indexUid); - const document = await this.getDocument(artist, language); - const task = await index.addDocuments([document], { - primaryKey: "id", - }); - await this.searchManager.waitForTask(task.taskUid); - } - } - - public async search(query: string, language: string = "zh") { - if (!this.searchManager) { - throw new Error("Search or embedding service not available"); - } - - const index = await this.searchManager.getSearchIndex(`artist_${language}`); - const embeddingResponse = await this.embeddingManager.embeddings.post({ - texts: [query], - }); - return index.search(query, { - vector: embeddingResponse?.data?.embeddings[0], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, - showRankingScore: true, - }); - } } diff --git a/packages/core/src/search/catalog/artistRole.ts b/packages/core/src/search/catalog/artistRole.ts new file mode 100644 index 0000000..c62e20e --- /dev/null +++ b/packages/core/src/search/catalog/artistRole.ts @@ -0,0 +1,33 @@ +import { ISearchService } from "../interface"; +import type { ArtistRoleDetailsResponseDto } from "../../modules"; + +export interface ArtistRoleSearchIndex { + id: number; + name: string; + localizedNames?: Record | null; + _vectors?: { + "potion-multilingual-128M": number[] | null; + }; +} + +export class ArtistRoleSearchService extends ISearchService< + ArtistRoleDetailsResponseDto, + ArtistRoleSearchIndex +> { + protected readonly entityType = "artistRole"; + + protected async getDocument( + role: ArtistRoleDetailsResponseDto, + language: string + ): Promise { + const vectors = await this.embeddingManager.embeddings.post({ + texts: [`Name: ${this.getName(role, language) ?? ""}`], + }); + return { + id: role.id, + name: this.getName(role, language) ?? "", + localizedNames: role.localizedNames, + _vectors: this.buildVectors(vectors.data?.embeddings[0]), + }; + } +} diff --git a/packages/core/src/search/catalog/index.ts b/packages/core/src/search/catalog/index.ts index 0f68be0..79d74fc 100644 --- a/packages/core/src/search/catalog/index.ts +++ b/packages/core/src/search/catalog/index.ts @@ -1,3 +1,4 @@ export * from "./song"; export * from "./artist"; +export * from "./artistRole"; export * from "./singer"; diff --git a/packages/core/src/search/catalog/singer.ts b/packages/core/src/search/catalog/singer.ts index 1084c62..79bede8 100644 --- a/packages/core/src/search/catalog/singer.ts +++ b/packages/core/src/search/catalog/singer.ts @@ -1,7 +1,5 @@ import { ISearchService } from "../interface"; -import { unique, keys } from "remeda"; import type { SingerDetailsResponseDto } from "../../modules"; -import { appLogger } from "@cvsa/logger"; export interface SingerSearchIndex { id: number; @@ -13,91 +11,29 @@ export interface SingerSearchIndex { }; } -export class SingerSearchService extends ISearchService { - private async getDocument( +export class SingerSearchService extends ISearchService< + SingerDetailsResponseDto, + SingerSearchIndex +> { + protected readonly entityType = "singer"; + + protected async getDocument( singer: SingerDetailsResponseDto, language: string ): Promise { - const getDesc = () => { - if (language === singer.language) return singer.description; - return singer.localizedDescriptions?.[language]; - }; - const getName = () => { - if (language === singer.language) return singer.name; - return singer.localizedNames?.[language]; - }; - const vectors = await this.embeddingManager.embeddings.post({ texts: [ - `Name: ${getName() ?? ""} -Description: ${getDesc() ?? ""} + `Name: ${this.getName(singer, language) ?? ""} +Description: ${this.getDescription(singer, language) ?? ""} `, ], }); return { id: singer.id, - name: getName() ?? undefined, - description: getDesc() ?? undefined, - _vectors: vectors.data?.embeddings[0] - ? { - "potion-multilingual-128M": vectors.data?.embeddings[0], - } - : { - "potion-multilingual-128M": null, - }, + name: this.getName(singer, language) ?? undefined, + language: singer.language, + description: this.getDescription(singer, language) ?? undefined, + _vectors: this.buildVectors(vectors.data?.embeddings[0]), }; } - - public async sync(id: number) { - if (!this.searchManager) { - appLogger.warn("Search service not available"); - return; - } - const singer = await this.repository.getDetailsById(id); - - if (!singer) { - const indexesToBeDeleted = - await this.searchManager.getLocalizedIndexesOfEntity("singer"); - for (const index of indexesToBeDeleted) { - const adminIndex = await this.searchManager.getAdminIndex(index); - const task = await adminIndex.deleteDocument(id); - await this.searchManager.waitForTask(task.taskUid); - } - return; - } - - const languages = unique([ - ...keys(singer.localizedNames ?? []), - ...keys(singer.localizedDescriptions ?? []), - singer.language, - ]); - for (const language of languages) { - const indexUid = `singer_${language}`; - const index = await this.searchManager.getAdminIndex(indexUid); - const document = await this.getDocument(singer, language); - const task = await index.addDocuments([document], { - primaryKey: "id", - }); - await this.searchManager.waitForTask(task.taskUid); - } - } - - public async search(query: string, language: string = "zh") { - if (!this.searchManager) { - throw new Error("Search or embedding service not available"); - } - - const index = await this.searchManager.getSearchIndex(`singer_${language}`); - const embeddingResponse = await this.embeddingManager.embeddings.post({ - texts: [query], - }); - return index.search(query, { - vector: embeddingResponse?.data?.embeddings[0], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, - showRankingScore: true, - }); - } } diff --git a/packages/core/src/search/catalog/song.ts b/packages/core/src/search/catalog/song.ts index 5449fb6..c405148 100644 --- a/packages/core/src/search/catalog/song.ts +++ b/packages/core/src/search/catalog/song.ts @@ -1,8 +1,6 @@ import type { SongType } from "@cvsa/db"; import { ISearchService } from "../interface"; -import { unique, keys } from "remeda"; import type { SongDetailsResponseDto } from "../../modules"; -import { appLogger } from "@cvsa/logger"; export interface SongSearchIndex { id: number; @@ -23,19 +21,17 @@ export interface SongSearchIndex { }; } -export class SongSearchService extends ISearchService { - private async getDocument( +export class SongSearchService extends ISearchService { + protected readonly entityType = "song"; + + protected async getDocument( song: SongDetailsResponseDto, language: string ): Promise { - const getDesc = () => { - if (language === song.language) return song.description; - return song.localizedDescriptions?.[language]; - }; const getLyrics = () => { return song.lyrics.find((item) => item.language === language)?.plainText; }; - const getLocalizedName = (attr: "singers" | "artists") => { + const getLocalizedRelatives = (attr: "singers" | "artists") => { const data = song[attr].map((item) => { if (item.language === language) { return item.name; @@ -45,20 +41,17 @@ export class SongSearchService extends ISearchService { return data.filter(Boolean) as string[]; }; const getSingers = () => { - return getLocalizedName("singers"); + return getLocalizedRelatives("singers"); }; const getArtists = () => { - return getLocalizedName("artists"); - }; - const getName = () => { - if (language === song.language) return song.name; - return song.localizedNames?.[language]; + return getLocalizedRelatives("artists"); }; + const vectors = await this.embeddingManager.embeddings.post({ texts: [ - `Name: ${getName() ?? ""} -Lyrics: ${song.lyrics ?? ""} -Description: ${getDesc() ?? ""} + `Name: ${this.getName(song, language) ?? ""} +Lyrics: ${getLyrics() ?? ""} +Description: ${this.getDescription(song, language) ?? ""} Singers: ${getSingers().join(", ")} Artists: ${getArtists().join(", ")} `, @@ -66,9 +59,9 @@ Artists: ${getArtists().join(", ")} }); return { id: song.id, - name: getName() ?? undefined, + name: this.getName(song, language) ?? undefined, lyrics: getLyrics() ?? undefined, - description: getDesc() ?? undefined, + description: this.getDescription(song, language) ?? undefined, singers: getSingers(), artists: getArtists(), bilibiliAid: song.bilibiliAid ?? undefined, @@ -78,68 +71,7 @@ Artists: ${getArtists().join(", ")} .map((item) => item.engine ?? undefined) .filter(Boolean) as string[], publishedAt: song.publishedAt ? new Date(song.publishedAt).getTime() : undefined, - _vectors: vectors.data?.embeddings[0] - ? { - "potion-multilingual-128M": vectors.data?.embeddings[0], - } - : { - "potion-multilingual-128M": null, - }, + _vectors: this.buildVectors(vectors.data?.embeddings[0]), }; } - - public async sync(id: number) { - if (!this.searchManager) { - appLogger.warn("Search service not available"); - return; - } - const song = await this.repository.getDetailsById(id); - - if (!song) { - const indexesToBeDeleted = await this.searchManager.getLocalizedIndexesOfEntity("song"); - for (const index of indexesToBeDeleted) { - const adminIndex = await this.searchManager.getAdminIndex(index); - const task = await adminIndex.deleteDocument(id); - await this.searchManager.waitForTask(task.taskUid); - } - return; - } - - const languages = unique([ - ...keys(song.localizedNames ?? []), - ...keys(song.localizedDescriptions ?? []), - song.language, - ]); - for (const language of languages) { - const indexUid = `song_${language}`; - const index = await this.searchManager.getAdminIndex(indexUid); - const document = await this.getDocument(song, language); - const task = await index.addDocuments([document], { - primaryKey: "id", - }); - await this.searchManager.waitForTask(task.taskUid); - } - } - - public async search(query: string, language: string = "zh") { - if (!this.searchManager) { - throw new Error("Search or embedding service not available"); - } - - const index = await this.searchManager.getSearchIndex(`song_${language}`); - const embeddingResponse = await this.embeddingManager.embeddings.post({ - texts: [query], - }); - const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0; - return index.search(query, { - vector: embeddingResponse?.data?.embeddings[0], - hybrid: embeddingAvailable - ? { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - } - : undefined, - showRankingScore: true, - }); - } } diff --git a/packages/core/src/search/config.ts b/packages/core/src/search/config.ts index 38937be..f662ea3 100644 --- a/packages/core/src/search/config.ts +++ b/packages/core/src/search/config.ts @@ -67,4 +67,14 @@ export const INDEX_SETTINGS: Record = { }, }, }, + artistRole: { + searchableAttributes: ["name"], + rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], + embedders: { + "potion-multilingual-128M": { + source: "userProvided", + dimensions: 256, + }, + }, + }, }; diff --git a/packages/core/src/search/interface.ts b/packages/core/src/search/interface.ts index 2b80ae5..8aeb662 100644 --- a/packages/core/src/search/interface.ts +++ b/packages/core/src/search/interface.ts @@ -2,13 +2,104 @@ import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; import type { SearchManager } from "./manager"; import type { SearchResponse } from "meilisearch"; import type { EmbeddingAppApi } from "@cvsa/embedding"; +import { unique, keys } from "remeda"; +import { appLogger } from "@cvsa/logger"; + +export interface LocalizableEntity { + id: number; + name?: string | null; + language: string; + description?: string | null; + localizedNames?: Record | null; + localizedDescriptions?: Record | null; +} + +export abstract class ISearchService { + protected abstract readonly entityType: string; -export abstract class ISearchService { constructor( protected readonly repository: IRepositoryWithGetDetails, protected readonly searchManager: SearchManager | undefined, protected readonly embeddingManager: EmbeddingAppApi ) {} - abstract sync(id: number): Promise; - abstract search(query: string, language: string): Promise; + + protected abstract getDocument(entity: T, language: string): Promise; + + protected getName(entity: T, language: string): string | undefined { + if (language === entity.language) return entity.name ?? undefined; + return entity.localizedNames?.[language]; + } + + protected getDescription(entity: T, language: string): string | undefined { + if (language === entity.language) return entity.description ?? undefined; + return entity.localizedDescriptions?.[language]; + } + + protected collectLanguages(entity: T): string[] { + return unique([ + ...keys(entity.localizedNames ?? {}), + ...keys(entity.localizedDescriptions ?? {}), + entity.language, + ]); + } + + protected buildVectors(embedding: number[] | undefined): { + "potion-multilingual-128M": number[] | null; + } { + return embedding + ? { "potion-multilingual-128M": embedding } + : { "potion-multilingual-128M": null }; + } + + public async sync(id: number): Promise { + if (!this.searchManager) { + appLogger.warn("Search service not available"); + return; + } + const entity = await this.repository.getDetailsById(id); + + if (!entity) { + const indexes = await this.searchManager.getLocalizedIndexesOfEntity(this.entityType); + for (const indexName of indexes) { + const adminIndex = await this.searchManager.getAdminIndex(indexName); + const task = await adminIndex.deleteDocument(id); + await this.searchManager.waitForTask(task.taskUid); + } + return; + } + + const languages = this.collectLanguages(entity); + for (const language of languages) { + const indexUid = `${this.entityType}_${language}`; + const index = await this.searchManager.getAdminIndex(indexUid); + const document = await this.getDocument(entity, language); + const task = await index.addDocuments([document], { + primaryKey: "id", + }); + await this.searchManager.waitForTask(task.taskUid); + } + } + + public async search(query: string, language: string = "zh"): Promise { + if (!this.searchManager) { + throw new Error("Search or embedding service not available"); + } + + const index = await this.searchManager.getSearchIndex(`${this.entityType}_${language}`); + const embeddingResponse = await this.embeddingManager.embeddings.post({ + texts: [query], + }); + const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0; + + return index.search(query, { + vector: embeddingResponse?.data?.embeddings[0], + hybrid: embeddingAvailable + ? { + embedder: "potion-multilingual-128M", + semanticRatio: 0.25, + } + : undefined, + showRankingScore: true, + }); + } } diff --git a/packages/core/tests/integration/ArtistRoleRepository.test.ts b/packages/core/tests/integration/ArtistRoleRepository.test.ts new file mode 100644 index 0000000..8b68954 --- /dev/null +++ b/packages/core/tests/integration/ArtistRoleRepository.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test } from "bun:test"; +import { + artistRoleRepository, + type CreateArtistRoleRequestDto, + type UpdateArtistRoleRequestDto, +} from "@cvsa/core"; + +const repository = artistRoleRepository; + +describe("ArtistRoleRepository Integration Tests", () => { + describe("create", () => { + test("should create an artist role with all fields", async () => { + const input: CreateArtistRoleRequestDto = { + name: "Producer", + localizedNames: { zh: "制作人", ja: "プロデューサー" }, + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("Producer"); + expect(result.localizedNames).toEqual({ zh: "制作人", ja: "プロデューサー" }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should create an artist role with minimal fields", async () => { + const input: CreateArtistRoleRequestDto = { + name: "Composer", + }; + + const result = await repository.create(input); + + expect(result).toBeDefined(); + expect(result.id).toBeGreaterThan(0); + expect(result.name).toBe("Composer"); + expect(result.localizedNames).toBeNull(); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + }); + + describe("getById", () => { + test("should return role when exists", async () => { + const created = await repository.create({ name: "Illustrator" }); + const result = await repository.getById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("Illustrator"); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when role does not exist", async () => { + const result = await repository.getById(999999); + expect(result).toBeNull(); + }); + }); + + describe("getDetailsById", () => { + test("should return role details when exists", async () => { + const created = await repository.create({ + name: "Lyricist", + localizedNames: { zh: "作词人" }, + }); + const result = await repository.getDetailsById(created.id); + + expect(result).toBeDefined(); + expect(result?.id).toBe(created.id); + expect(result?.name).toBe("Lyricist"); + expect(result?.localizedNames).toEqual({ zh: "作词人" }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result?.deletedAt).not.toBeDefined(); + }); + + test("should return null when role does not exist", async () => { + const result = await repository.getDetailsById(999999); + expect(result).toBeNull(); + }); + }); + + describe("update", () => { + test("should update all fields", async () => { + const created = await repository.create({ + name: "Old Role", + localizedNames: { zh: "旧角色" }, + }); + + const input: UpdateArtistRoleRequestDto = { + name: "New Role", + localizedNames: { zh: "新角色", en: "New Role" }, + }; + + const result = await repository.update(created.id, input); + + expect(result.name).toBe("New Role"); + expect(result.localizedNames).toEqual({ zh: "新角色", en: "New Role" }); + //@ts-expect-error accessing nonexistent field for testing purpose + expect(result.deletedAt).not.toBeDefined(); + }); + + test("should update only specified fields", async () => { + const created = await repository.create({ + name: "Original Name", + localizedNames: { zh: "原名" }, + }); + + const result = await repository.update(created.id, { name: "Updated Name" }); + + expect(result.name).toBe("Updated Name"); + expect(result.localizedNames).toEqual({ zh: "原名" }); + }); + }); + + describe("softDelete", () => { + test("should soft delete a role", async () => { + const created = await repository.create({ name: "Role to Delete" }); + + await repository.softDelete(created.id); + + const result = await repository.getById(created.id); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/core/tests/unit/ArtistRoleService.test.ts b/packages/core/tests/unit/ArtistRoleService.test.ts new file mode 100644 index 0000000..60568cb --- /dev/null +++ b/packages/core/tests/unit/ArtistRoleService.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, mock, test } from "bun:test"; +import { prisma } from "@cvsa/db"; +import { AppError } from "../../src/error/AppError"; +import { ArtistRoleService } from "../../src/modules/catalog/artistRole/service"; +import type { ArtistRoleDetailsResponseDto } from "../../src/modules/catalog/artistRole/dto"; +import type { IArtistRoleRepository } from "../../src/modules/catalog/artistRole/repository.interface"; +import type { OutboxService } from "../../src/modules/outbox/service"; +import { createMockRepository } from "../utils"; + +( + prisma as unknown as { + $transaction: (fn: (tx: unknown) => Promise) => Promise; + } +).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); + +const mockArtistRole: ArtistRoleDetailsResponseDto = { + id: 1, + name: "Producer", + language: "zh", + localizedNames: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const mockOutboxEntry = { + id: 1, + aggregateType: "artistRole" as const, + aggregateId: 1, + eventType: "artistRole.created" as const, + payload: null, + status: "PENDING" as const, + retryCount: 0, + lastError: null, + nextRetryAt: null, + createdAt: new Date().toISOString(), + processedAt: null, +}; + +describe("ArtistRoleService", () => { + const mockRepository = createMockRepository({ + getById: async (id: number) => { + if (id === 1) { + return mockArtistRole; + } + return null; + }, + getDetailsById: async (id: number) => { + if (id === 1) { + return mockArtistRole; + } + return null; + }, + create: async () => mockArtistRole, + update: async () => mockArtistRole, + softDelete: async () => {}, + }); + + const mockOutboxService = { + createEntry: mock(async () => mockOutboxEntry), + enqueue: mock(async () => {}), + processEntry: mock(async () => {}), + recoverStaleEntries: mock(async () => {}), + }; + + const artistRoleService = new ArtistRoleService( + mockRepository as unknown as IArtistRoleRepository, + mockOutboxService as unknown as OutboxService + ); + + describe("getDetails", () => { + test("returns artist role when role exists", async () => { + const result = await artistRoleService.getDetails(1); + + expect(result).toEqual(mockArtistRole); + expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); + }); + + test("throws NOT_FOUND error when role does not exist", async () => { + mockRepository.getDetailsById.mockResolvedValueOnce(null); + + expect(artistRoleService.getDetails(999)).rejects.toThrow(AppError); + expect(artistRoleService.getDetails(999)).rejects.toThrow("error.artistRole.notfound"); + expect(artistRoleService.getDetails(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("create", () => { + const createInput = { + name: "Composer", + }; + + test("creates role and calls outbox.createEntry and outbox.enqueue", async () => { + const result = await artistRoleService.create(createInput); + + expect(result).toMatchObject({ + name: "Producer", + }); + expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artistRole", + aggregateId: mockArtistRole.id, + eventType: "artistRole.created", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + }); + + describe("update", () => { + const updateInput = { name: "Lyricist" }; + + test("updates role and calls outbox.createEntry and outbox.enqueue on success", async () => { + const result = await artistRoleService.update(1, updateInput); + + expect(result).toMatchObject({ + name: "Producer", + }); + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artistRole", + aggregateId: 1, + eventType: "artistRole.updated", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when role does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(artistRoleService.update(999, updateInput)).rejects.toThrow(AppError); + expect(artistRoleService.update(999, updateInput)).rejects.toThrow( + "error.artistRole.notfound" + ); + expect(artistRoleService.update(999, updateInput)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); + + describe("delete", () => { + test("soft deletes role and calls outbox.createEntry and outbox.enqueue on success", async () => { + await artistRoleService.delete(1); + + expect(mockRepository.getById).toHaveBeenCalledWith(1); + expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); + expect(mockOutboxService.createEntry).toHaveBeenCalledWith( + { + aggregateType: "artistRole", + aggregateId: 1, + eventType: "artistRole.deleted", + }, + expect.anything() + ); + expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); + }); + + test("throws NOT_FOUND error when role does not exist", async () => { + mockRepository.getById.mockResolvedValueOnce(null); + + expect(artistRoleService.delete(999)).rejects.toThrow(AppError); + expect(artistRoleService.delete(999)).rejects.toThrow("error.artistRole.notfound"); + expect(artistRoleService.delete(999)).rejects.toMatchObject({ + code: "NOT_FOUND", + statusCode: 404, + }); + }); + }); +}); diff --git a/packages/core/tests/unit/ArtistSearchService.test.ts b/packages/core/tests/unit/ArtistSearchService.test.ts index c3fdeaf..e71742a 100644 --- a/packages/core/tests/unit/ArtistSearchService.test.ts +++ b/packages/core/tests/unit/ArtistSearchService.test.ts @@ -233,10 +233,7 @@ describe("ArtistSearchService", () => { expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { vector: undefined, - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, + hybrid: undefined, showRankingScore: true, }); }); diff --git a/packages/core/tests/unit/SingerSearchService.test.ts b/packages/core/tests/unit/SingerSearchService.test.ts index 54a81a5..a76c35a 100644 --- a/packages/core/tests/unit/SingerSearchService.test.ts +++ b/packages/core/tests/unit/SingerSearchService.test.ts @@ -232,10 +232,7 @@ describe("SingerSearchService", () => { expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { vector: undefined, - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, + hybrid: undefined, showRankingScore: true, }); }); diff --git a/packages/core/tests/unit/SongSearchService.test.ts b/packages/core/tests/unit/SongSearchService.test.ts index fd51fa1..b5e585a 100644 --- a/packages/core/tests/unit/SongSearchService.test.ts +++ b/packages/core/tests/unit/SongSearchService.test.ts @@ -81,6 +81,7 @@ const mockSongDetails: SongDetailsResponseDto = { aliases: [], userId: null, role: { + language: "zh", id: 1, name: "", localizedNames: null, diff --git a/packages/db/prisma/models/core/artistRole.prisma b/packages/db/prisma/models/core/artistRole.prisma index 1b97f64..046bbac 100644 --- a/packages/db/prisma/models/core/artistRole.prisma +++ b/packages/db/prisma/models/core/artistRole.prisma @@ -3,6 +3,7 @@ model ArtistRole { name String /// [LocalizedField] localizedNames Json? @map("localized_names") + language String @default("zh") creations Creation[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/packages/db/prisma/models/core/song.prisma b/packages/db/prisma/models/core/song.prisma index 6ad9f4f..f8b6c1a 100644 --- a/packages/db/prisma/models/core/song.prisma +++ b/packages/db/prisma/models/core/song.prisma @@ -37,6 +37,9 @@ model Song { enum SongType { ORIGINAL COVER + RETUNE + CONTRAFACTUM + TRANSLYRICS REMIX REMASTER MASHUP diff --git a/packages/embedding/package.json b/packages/embedding/package.json index 0e412c7..9589927 100644 --- a/packages/embedding/package.json +++ b/packages/embedding/package.json @@ -14,7 +14,7 @@ "@huggingface/hub": "^2.11.0", "@huggingface/transformers": "^4.0.1", "elysia": "latest", - "onnxruntime-node": "^1.24.3", + "onnxruntime-node": "1.24.3", "zod": "^4.3.6" }, "devDependencies": { diff --git a/packages/embedding/src/embedding.ts b/packages/embedding/src/embedding.ts index 7c7a744..fbc38f8 100644 --- a/packages/embedding/src/embedding.ts +++ b/packages/embedding/src/embedding.ts @@ -126,7 +126,7 @@ export class EmbeddingManager { const tokenizer = await this.getTokenizer(); const session = await this.getModelSession(); - const { input_ids } = await tokenizer(texts, { + const { input_ids } = tokenizer(texts, { add_special_tokens: false, return_tensor: false, }); @@ -143,7 +143,7 @@ export class EmbeddingManager { const offsets: number[] = [ 0, - ...cumsum(input_ids.slice(0, -1).map((x: string) => x.length)), + ...cumsum(input_ids.slice(0, -1).map((x: number[]) => x.length)), ]; const flattened_input_ids = input_ids.flat(); diff --git a/scripts/import.ts b/scripts/import.ts new file mode 100644 index 0000000..7085f49 --- /dev/null +++ b/scripts/import.ts @@ -0,0 +1,195 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { extname, join, resolve } from "node:path"; +import { prisma } from "@cvsa/db"; +import { songService, artistService, singerService, artistRoleService } from "@cvsa/core"; + +interface JsonFile { + type: "song" | "album" | "series" | "singer" | "artist" | "others"; + name?: string; + songType?: string; + singers?: { name: string }[]; + artists?: { name: string; role: string }[]; + lyricsText?: string; + producer?: string[]; + bilibiliID?: string; + youtubeID?: string; + niconicoID?: string; + description?: string; + uploader?: string; + publishedAt?: string; + lyricsTTML?: string; +} + +const songTypeMap: Record = { + original: "ORIGINAL", + cover: "COVER", + retune: "RETUNE", + contrafactum: "CONTRAFACTUM", + translyrics: "TRANSLYRICS", + remix: "REMIX", + remaster: "REMASTER", + mashup: "MASHUP", + instrumental: "INSTRUMENTAL", + others: "OTHERS", +}; + +async function resolveSinger(name: string) { + const existing = await prisma.singer.findFirst({ where: { name, deletedAt: null } }); + if (existing) return existing.id; + const created = await singerService.create({ name }); + return created.id; +} + +async function resolveArtist(name: string) { + const existing = await prisma.artist.findFirst({ where: { name, deletedAt: null } }); + if (existing) return existing.id; + const created = await artistService.create({ name }); + return created.id; +} + +async function resolveRole(name: string) { + const existing = await prisma.artistRole.findFirst({ where: { name, deletedAt: null } }); + if (existing) return existing.id; + const created = await artistRoleService.create({ name }); + return created.id; +} + +async function importSinger(data: JsonFile) { + const result = await singerService.create({ name: data.name }); + console.log(`singer → ${result.id}`); +} + +async function importArtist(data: JsonFile) { + const result = await artistService.create({ name: data.name }); + console.log(`artist → ${result.id}`); +} + +async function importSong(data: JsonFile) { + const performances: { singerId: number }[] = []; + if (data.singers) { + for (const s of data.singers) { + performances.push({ singerId: await resolveSinger(s.name) }); + } + } + + const creations: { artistId: number; roleId: number }[] = []; + if (data.artists) { + for (const a of data.artists) { + creations.push({ + artistId: await resolveArtist(a.name), + roleId: await resolveRole(a.role), + }); + } + } + if (data.producer) { + const roleId = await resolveRole("Producer"); + for (const name of data.producer) { + creations.push({ artistId: await resolveArtist(name), roleId }); + } + } + if (data.uploader) { + const roleId = await resolveRole("Uploader"); + creations.push({ artistId: await resolveArtist(data.uploader), roleId }); + } + + const lyrics: { plainText?: string; ttml?: string; language: string }[] = []; + if (data.lyricsText) lyrics.push({ plainText: data.lyricsText, language: "zh" }); + if (data.lyricsTTML) { + const existing = lyrics.find((l) => l.language === "zh" && l.plainText); + if (existing) existing.ttml = data.lyricsTTML; + else lyrics.push({ ttml: data.lyricsTTML, language: "zh" }); + } + + const song = await songService.create({ + name: data.name, + type: data.songType ? (songTypeMap[data.songType] as never) : undefined, + description: data.description, + publishedAt: new Date(data.publishedAt ?? "").toISOString(), + performances: performances.length ? performances : undefined, + creations: creations.length ? creations : undefined, + lyrics: lyrics.length ? lyrics : undefined, + }); + + if (data.bilibiliID) { + const updateData: Record = {}; + if (/^BV/i.test(data.bilibiliID)) updateData.bilibiliBvid = data.bilibiliID; + else if (/^\d+$/.test(data.bilibiliID)) updateData.bilibiliAid = BigInt(data.bilibiliID); + if (Object.keys(updateData).length) { + await prisma.song.update({ where: { id: song.id }, data: updateData }); + } + } + + if (data.youtubeID) { + await prisma.externalLink.create({ + data: { + songId: song.id, + url: `https://youtube.com/watch?v=${data.youtubeID}`, + platform: "YOUTUBE", + platformId: data.youtubeID, + }, + }); + } + if (data.niconicoID) { + await prisma.externalLink.create({ + data: { + songId: song.id, + url: `https://nicovideo.jp/watch/${data.niconicoID}`, + platform: "NICONICO", + platformId: data.niconicoID, + }, + }); + } + + console.log(`song → ${song.id}`); +} + +async function main() { + const folder = process.argv[2]; + if (!folder) { + console.error("Usage: bun run scripts/import.ts "); + process.exit(1); + } + + const folderPath = resolve(folder); + if (!existsSync(folderPath)) { + console.error("not found"); + process.exit(1); + } + + const files = readdirSync(folderPath) + .filter((f) => extname(f) === ".json") + .sort(); + + for (const file of files) { + try { + const data: JsonFile = JSON.parse(readFileSync(join(folderPath, file), "utf-8")); + switch (data.type) { + case "singer": + await importSinger(data); + break; + case "artist": + await importArtist(data); + break; + case "song": + await importSong(data); + break; + default: + await importSinger(data); + console.warn(`unkown type ${data.type} for file ${file}`); + } + } catch (e) { + console.error("error at: ", file); + console.error(e); + } + } +} + +main() + .catch((err) => { + console.error("fatal:", err); + process.exit(1); + }) + .finally(() => { + console.log("completed"); + process.exit(0); + }); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..0d3e8db --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../packages/typescript-config/base.json", + "compilerOptions": { + "noEmit": true, + "paths": { + "@cvsa/db": ["../packages/db/src"], + "@cvsa/core": ["../packages/core/src"], + "@cvsa/core/internal": ["../packages/core/src/internal"], + "@cvsa/env": ["../packages/env/src"], + "@cvsa/logger": ["../packages/logger/src"], + "@cvsa/observability": ["../packages/observability/src"], + "@cvsa/embedding": ["../packages/embedding/src"] + } + }, + "include": ["./**/*.ts"] +} From 2d37c224e860a4b8799dceea8b37ae7febdc0b72 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 9 Jun 2026 04:43:56 +0800 Subject: [PATCH 29/37] ci(docs): configure dynamic site URL and auto-generate sitemap 1. Add an environment variable config file to manage the site URL centrally 2. Inject the site URL environment variable during CI builds 3. Replace hardcoded site URLs with dynamically loaded configurations 4. Implement automatic sitemap generation logic and remove the manually maintained sitemap file --- .github/workflows/build-docs.yml | 4 +- .trae/rules/git-commit-message.md | 1 + .../app/(docs)/[lang]/[[...slug]]/page.tsx | 10 +- apps/docs/app/(landing)/layout.tsx | 13 +- apps/docs/app/sitemap.ts | 28 +++++ apps/docs/lib/env.ts | 7 ++ apps/docs/public/sitemap.xml | 111 ------------------ 7 files changed, 50 insertions(+), 124 deletions(-) create mode 100644 apps/docs/app/sitemap.ts create mode 100644 apps/docs/lib/env.ts delete mode 100644 apps/docs/public/sitemap.xml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index e07b9a2..0362c66 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -30,6 +30,8 @@ jobs: - name: Build docs app run: bun run build --filter=docs + env: + NEXT_PUBLIC_SITE_URL: https://docs.projectcvsa.com - name: Verify build output run: | @@ -43,4 +45,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy apps/docs/out --project-name="cvsa-docs" + command: pages deploy apps/docs/out --project-name="cvsa-docs" \ No newline at end of file diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md index 6536d5c..89ec985 100644 --- a/.trae/rules/git-commit-message.md +++ b/.trae/rules/git-commit-message.md @@ -1,6 +1,7 @@ --- alwaysApply: true scene: git_message +language: en_US --- ### 基本格式 diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index ca0d062..e334567 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -8,6 +8,7 @@ import { gitConfig } from "@/lib/layout.shared"; import { MarkdownCopyButton, ViewOptionsPopover } from "@/components/page-actions"; import { buttonVariants } from "fumadocs-ui/components/ui/button"; import { t } from "@/lib/i18n"; +import { siteUrl } from "@/lib/env"; export async function generateStaticParams() { const pages = source.getPages(); @@ -79,20 +80,19 @@ export async function generateMetadata({ }): Promise { const { slug, lang } = await params; const page = source.getPage(slug, lang); - const baseUrl = "https://docs.projectcvsa.com"; if (!page) notFound(); const cleanSlug = slug ? slug : []; const currentPage = cleanSlug.join("/"); - const url = `${baseUrl}/${lang}/${currentPage}`; + const url = `${siteUrl}/${lang}/${currentPage}`; return { title: page.data.title, description: page.data.description, alternates: { canonical: url, languages: { - zh: `${baseUrl}/zh/${currentPage}`, - en: `${baseUrl}/en/${currentPage}`, - "x-default": `${baseUrl}/en/${currentPage}`, + zh: `${siteUrl}/zh/${currentPage}`, + en: `${siteUrl}/en/${currentPage}`, + "x-default": `${siteUrl}/en/${currentPage}`, }, }, openGraph: { diff --git a/apps/docs/app/(landing)/layout.tsx b/apps/docs/app/(landing)/layout.tsx index 0b10235..42f40f0 100644 --- a/apps/docs/app/(landing)/layout.tsx +++ b/apps/docs/app/(landing)/layout.tsx @@ -2,30 +2,29 @@ import "./global.css"; import { Funnel_Sans } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; import type { Metadata } from "next"; +import { siteUrl } from "@/lib/env"; const inter = Funnel_Sans({ subsets: ["latin"], }); -const baseUrl = "https://docs.projectcvsa.com"; - export const metadata: Metadata = { title: "Project CVSA | Archive for a Better Future", description: "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", alternates: { - canonical: baseUrl, + canonical: siteUrl, languages: { - en: `${baseUrl}/en`, - zh: `${baseUrl}/zh`, - "x-default": `${baseUrl}/en`, + en: `${siteUrl}/en`, + zh: `${siteUrl}/zh`, + "x-default": `${siteUrl}/en`, }, }, openGraph: { title: "Project CVSA | Archive for a Better Future", description: "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", - url: baseUrl, + url: siteUrl, siteName: "Project CVSA", locale: "en_US", type: "website", diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts new file mode 100644 index 0000000..72ec61b --- /dev/null +++ b/apps/docs/app/sitemap.ts @@ -0,0 +1,28 @@ +import type { MetadataRoute } from "next"; +import { source } from "@/lib/source"; +import { siteUrl } from "@/lib/env"; + +export default function sitemap(): MetadataRoute.Sitemap { + const pages = source.getPages(); + + const docPages = pages.map((page) => { + const slug = page.slugs.join("/"); + const url = `${siteUrl}/${page.locale}${slug ? `/${slug}` : ""}`; + + return { + url, + lastModified: new Date(), + changeFrequency: "weekly" as const, + priority: page.slugs.length === 0 ? 0.8 : 0.6, + }; + }); + + const landingPage: MetadataRoute.Sitemap[number] = { + url: siteUrl, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 1.0, + }; + + return [landingPage, ...docPages]; +} \ No newline at end of file diff --git a/apps/docs/lib/env.ts b/apps/docs/lib/env.ts new file mode 100644 index 0000000..c490aa5 --- /dev/null +++ b/apps/docs/lib/env.ts @@ -0,0 +1,7 @@ +/** + * Site URL configuration. + * Set NEXT_PUBLIC_SITE_URL in CI/CD or .env.local to override. + * Falls back to production URL during build. + */ +export const siteUrl = + process.env.NEXT_PUBLIC_SITE_URL || "https://docs.projectcvsa.com"; \ No newline at end of file diff --git a/apps/docs/public/sitemap.xml b/apps/docs/public/sitemap.xml deleted file mode 100644 index 4749086..0000000 --- a/apps/docs/public/sitemap.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - https://docs.projectcvsa.com/ - 2026-05-16T13:46:34+00:00 - 1.00 - - - https://docs.projectcvsa.com/en - 2026-05-16T13:46:34+00:00 - 0.80 - - - https://docs.projectcvsa.com/zh - 2026-05-16T13:46:34+00:00 - 0.64 - - - https://docs.projectcvsa.com/zh/scope-of-inclusion - 2026-05-16T13:46:34+00:00 - 0.51 - - - https://docs.projectcvsa.com/zh/internal - 2026-05-16T13:46:34+00:00 - 0.51 - - - https://docs.projectcvsa.com/zh/internal/developers/guide - 2026-05-16T13:46:34+00:00 - 0.41 - - - https://docs.projectcvsa.com/zh/internal/developers/architecture - 2026-05-16T13:46:34+00:00 - 0.41 - - - https://docs.projectcvsa.com/zh/internal/developers/hld - 2026-05-16T13:46:34+00:00 - 0.33 - - - https://docs.projectcvsa.com/zh/internal/developers/style-guide - 2026-05-16T13:46:34+00:00 - 0.33 - - - https://docs.projectcvsa.com/zh/internal/developers/workflow - 2026-05-16T13:46:34+00:00 - 0.33 - - - https://docs.projectcvsa.com/zh/internal/developers/crawler/overview - 2026-05-16T13:46:34+00:00 - 0.26 - - - https://docs.projectcvsa.com/zh/internal/legacy/architecture-overview - 2026-05-16T13:46:34+00:00 - 0.21 - - - https://docs.projectcvsa.com/zh/internal/legacy/crawling-lifecycle - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/video-discovery-pipeline - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/song-collection-pipeline - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/snapshot-scheduling - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/snapshot-execution - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/message-queue-reference - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/database-schema-reference - 2026-05-16T13:46:34+00:00 - 0.17 - - - https://docs.projectcvsa.com/zh/internal/legacy/network-proxy-architecture - 2026-05-16T13:46:34+00:00 - 0.17 - - - - \ No newline at end of file From cd22fb22e3a94fff7294ff49ed7294ccd2f62816 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 9 Jun 2026 06:00:08 +0800 Subject: [PATCH 30/37] refactor(docs): refactor site metadata and robots configuration 1. Remove legacy static `robots.txt` and dynamically generate the robots config using Next.js MetadataRoute 2. Introduce a metadata utility library to centralize the management of site titles, descriptions, and OG image configurations 3. Optimize site URL handling in environment variables by automatically stripping trailing slashes 4. Add multi-language site name configurations to unify site name retrieval across metadata 5. Apply standardized OpenGraph image configurations to all page metadata 6. Comment out ineffective Twitter Card configurations to unify the metadata format --- .../app/(docs)/[lang]/[[...slug]]/page.tsx | 23 +++++++++---- apps/docs/app/(landing)/layout.tsx | 34 ++++++++++++------- apps/docs/app/not-found.tsx | 21 +++++++++++- apps/docs/app/robots.ts | 12 +++++++ apps/docs/lib/env.ts | 4 +-- apps/docs/lib/i18n.ts | 2 ++ apps/docs/lib/metadata.ts | 20 +++++++++++ apps/docs/lib/source.ts | 9 ----- apps/docs/public/robots.txt | 4 --- 9 files changed, 94 insertions(+), 35 deletions(-) create mode 100644 apps/docs/app/robots.ts create mode 100644 apps/docs/lib/metadata.ts delete mode 100644 apps/docs/public/robots.txt diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index e334567..7661edd 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -9,6 +9,7 @@ import { MarkdownCopyButton, ViewOptionsPopover } from "@/components/page-action import { buttonVariants } from "fumadocs-ui/components/ui/button"; import { t } from "@/lib/i18n"; import { siteUrl } from "@/lib/env"; +import { ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; export async function generateStaticParams() { const pages = source.getPages(); @@ -84,6 +85,7 @@ export async function generateMetadata({ const cleanSlug = slug ? slug : []; const currentPage = cleanSlug.join("/"); const url = `${siteUrl}/${lang}/${currentPage}`; + const ogImage = ogImageUrl(); return { title: page.data.title, description: page.data.description, @@ -99,14 +101,23 @@ export async function generateMetadata({ title: page.data.title, description: page.data.description, url, - siteName: "Project CVSA", + siteName: t("siteName", lang), locale: lang === "zh" ? "zh_CN" : "en_US", type: "article", + images: [ + { + url: ogImage, + width: OG_IMAGE_SIZE, + height: OG_IMAGE_SIZE, + alt: page.data.title, + }, + ], }, - twitter: { - card: "summary_large_image", - title: page.data.title, - description: page.data.description, - }, + // twitter: { + // card: "summary_large_image", + // title: page.data.title, + // description: page.data.description, + // images: [ogImage], + // }, }; } diff --git a/apps/docs/app/(landing)/layout.tsx b/apps/docs/app/(landing)/layout.tsx index 42f40f0..cda2276 100644 --- a/apps/docs/app/(landing)/layout.tsx +++ b/apps/docs/app/(landing)/layout.tsx @@ -3,15 +3,16 @@ import { Funnel_Sans } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; import type { Metadata } from "next"; import { siteUrl } from "@/lib/env"; +import { t } from "@/lib/i18n"; +import { SITE_TITLE, SITE_DESCRIPTION, ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; const inter = Funnel_Sans({ subsets: ["latin"], }); export const metadata: Metadata = { - title: "Project CVSA | Archive for a Better Future", - description: - "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", + title: SITE_TITLE, + description: SITE_DESCRIPTION, alternates: { canonical: siteUrl, languages: { @@ -21,20 +22,27 @@ export const metadata: Metadata = { }, }, openGraph: { - title: "Project CVSA | Archive for a Better Future", - description: - "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", + title: SITE_TITLE, + description: SITE_DESCRIPTION, url: siteUrl, - siteName: "Project CVSA", + siteName: t("siteName", "en"), locale: "en_US", type: "website", + images: [ + { + url: ogImageUrl(), + width: OG_IMAGE_SIZE, + height: OG_IMAGE_SIZE, + alt: SITE_TITLE, + }, + ], }, - twitter: { - card: "summary_large_image", - title: "Project CVSA | Archive for a Better Future", - description: - "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community.", - }, + // twitter: { + // card: "summary_large_image", + // title: SITE_TITLE, + // description: SITE_DESCRIPTION, + // images: [ogImageUrl()], + // }, }; export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/apps/docs/app/not-found.tsx b/apps/docs/app/not-found.tsx index 0e8d6f2..ad5aa7a 100644 --- a/apps/docs/app/not-found.tsx +++ b/apps/docs/app/not-found.tsx @@ -2,15 +2,34 @@ import "./global.css"; import { Inter } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; import type { Metadata } from "next"; +import { SITE_TITLE, ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; const inter = Inter({ subsets: ["latin"], }); export const metadata: Metadata = { - title: "Not Found - Project CVSA", + title: `Not Found - ${SITE_TITLE}`, description: "The page you are looking for does not exist.", robots: { index: false, follow: false }, + openGraph: { + title: `Not Found - ${SITE_TITLE}`, + description: "The page you are looking for does not exist.", + images: [ + { + url: ogImageUrl(), + width: OG_IMAGE_SIZE, + height: OG_IMAGE_SIZE, + alt: SITE_TITLE, + }, + ], + }, + // twitter: { + // card: "summary_large_image", + // title: `Not Found - ${SITE_TITLE}`, + // description: "The page you are looking for does not exist.", + // images: [ogImageUrl()], + // }, }; export default function NotFound() { diff --git a/apps/docs/app/robots.ts b/apps/docs/app/robots.ts new file mode 100644 index 0000000..98797f7 --- /dev/null +++ b/apps/docs/app/robots.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from "next"; +import { siteUrl } from "@/lib/env"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: `${siteUrl}/sitemap.xml`, + }; +} diff --git a/apps/docs/lib/env.ts b/apps/docs/lib/env.ts index c490aa5..956f517 100644 --- a/apps/docs/lib/env.ts +++ b/apps/docs/lib/env.ts @@ -3,5 +3,5 @@ * Set NEXT_PUBLIC_SITE_URL in CI/CD or .env.local to override. * Falls back to production URL during build. */ -export const siteUrl = - process.env.NEXT_PUBLIC_SITE_URL || "https://docs.projectcvsa.com"; \ No newline at end of file +const raw = process.env.NEXT_PUBLIC_SITE_URL || "https://docs.projectcvsa.com"; +export const siteUrl = raw.replace(/\/$/, ""); \ No newline at end of file diff --git a/apps/docs/lib/i18n.ts b/apps/docs/lib/i18n.ts index e1f5f83..5b3350c 100644 --- a/apps/docs/lib/i18n.ts +++ b/apps/docs/lib/i18n.ts @@ -13,6 +13,7 @@ export const i18n = defineI18n({ const resources = { en: { title: "Project CVSA | Archive for a Better Future", + siteName: "Project CVSA", more: "More", viewAsMarkdown: "View as Markdown", openInGitHub: "Open in GitHub", @@ -23,6 +24,7 @@ const resources = { }, zh: { title: "中V档案馆 | Archive for a Better Future", + siteName: "中V档案馆", more: "更多", viewAsMarkdown: "查看 Markdown 源码", openInGitHub: "在 GitHub 中打开", diff --git a/apps/docs/lib/metadata.ts b/apps/docs/lib/metadata.ts new file mode 100644 index 0000000..76dda59 --- /dev/null +++ b/apps/docs/lib/metadata.ts @@ -0,0 +1,20 @@ +import { siteUrl } from "./env"; + +/** Shared title used across OG tags */ +export const SITE_TITLE = "Project CVSA | Archive for a Better Future"; + +/** Shared description used across OG tags */ +export const SITE_DESCRIPTION = + "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community."; + +/** OG image dimensions (android-chrome-192x192.png) */ +export const OG_IMAGE_SIZE = 192; + +/** + * Returns the full OG image URL. + * Uses the static android-chrome-192x192.png icon. + * No hardcoded URLs — uses siteUrl from env. + */ +export function ogImageUrl(): string { + return `${siteUrl}/android-chrome-192x192.png`; +} \ No newline at end of file diff --git a/apps/docs/lib/source.ts b/apps/docs/lib/source.ts index 04ac8f9..7dc8565 100644 --- a/apps/docs/lib/source.ts +++ b/apps/docs/lib/source.ts @@ -11,15 +11,6 @@ export const source = loader({ plugins: [lucideIconsPlugin()], }); -export function getPageImage(page: InferPageType) { - const segments = [...page.slugs, "image.webp"]; - - return { - segments, - url: `/og/docs/${segments.join("/")}`, - }; -} - export async function getLLMText(page: InferPageType) { return page.data.getText("raw"); } diff --git a/apps/docs/public/robots.txt b/apps/docs/public/robots.txt deleted file mode 100644 index e2b08c0..0000000 --- a/apps/docs/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://docs.projectcvsa.com/sitemap.xml \ No newline at end of file From f9bb9c738ffb27bdc1d02920f7c6fd9e86c99ff0 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 01:27:30 +0800 Subject: [PATCH 31/37] feat(llms-docs): add structured header and proper caching for LLM documentation endpoints 1. Add a unified project header to both the llms.txt and llms-full.txt endpoints, including the site link and usage instructions. 2. Refactor the llms-full.txt endpoint to organize document content by language and add standardized title links. 3. Add cache-control headers to both endpoints, setting the cache expiration time to 1 hour. 4. Add internationalization (i18n) support to the llms-full.txt endpoint to accommodate site names in both Chinese and English. --- apps/docs/app/llms-full.txt/route.ts | 65 ++++++++++++++++++++++++++-- apps/docs/app/llms.txt/route.ts | 35 ++++++++++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts index b5b8fe6..e74862f 100644 --- a/apps/docs/app/llms-full.txt/route.ts +++ b/apps/docs/app/llms-full.txt/route.ts @@ -1,10 +1,69 @@ import { getLLMText, source } from "@/lib/source"; +import { siteUrl } from "@/lib/env"; +import { t } from "@/lib/i18n"; export const revalidate = false; +const HEADER = `# Project CVSA | 中V档案馆 — Full Documentation + +> Project CVSA (中V档案馆) is an archive program that collects and preserves +> information about the Chinese singing voice synthesis community. This file +> contains the full markdown content of every documentation page on this site, +> concatenated for easy ingestion by LLMs and other automated tools. + +## How to use this file + +- The content is grouped by language, then by page. +- Each page is preceded by a level-2 heading with its title and URL. +- Pages within a language are separated by \`---\` dividers. +- Prefer citing the canonical page URL (linked in each section heading) over + copying text from this file, so links stay current. + +## Project links + +- Project home: ${siteUrl} +- English docs: ${siteUrl}/en +- 中文文档: ${siteUrl}/zh +- Index of all pages: ${siteUrl}/llms.txt +- Markdown source of any single page: append \`/raw//.mdx\` to the site URL +- Sitemap: ${siteUrl}/sitemap.xml + +--- + +`; + +const SECTION_DIVIDER = "\n\n---\n\n"; + export async function GET() { - const scan = source.getPages().map(getLLMText); - const scanned = await Promise.all(scan); + const pages = source.getPages(); + const sections: string[] = []; + + for (const lang of ["en", "zh"] as const) { + const langPages = pages.filter((p) => p.locale === lang); + if (langPages.length === 0) continue; + + const contents = await Promise.all(langPages.map(getLLMText)); + + const blocks = langPages.map((page, i) => { + const slug = page.slugs.join("/"); + const url = slug + ? `${siteUrl}/${lang}/${slug}` + : `${siteUrl}/${lang}`; + const title = page.data.title ?? slug; + return `## [${title}](${url})\n\n${contents[i]}`; + }); + + sections.push( + `# ${t("siteName", lang)} — ${lang.toUpperCase()}\n\n${blocks.join(SECTION_DIVIDER)}`, + ); + } + + const body = `${HEADER}${sections.join(SECTION_DIVIDER)}`; - return new Response(scanned.join("\n\n")); + return new Response(body, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "public, max-age=3600", + }, + }); } diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 0719403..3c5bc25 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -1,8 +1,41 @@ import { source } from "@/lib/source"; import { llms } from "fumadocs-core/source"; +import { siteUrl } from "@/lib/env"; export const revalidate = false; +const HEADER = `# Project CVSA | 中V档案馆 + +> Project CVSA (中V档案馆) is an archive program that collects and preserves +> information about the Chinese singing voice synthesis community. This site +> documents the project's mission, data model, architecture, and contribution +> guidelines for both English and Chinese readers. + +This file is an LLM-friendly index of every documentation page on this site. +Each entry links to a page and summarizes what it covers. + +- Project home: ${siteUrl} +- English docs: ${siteUrl}/en +- 中文文档: ${siteUrl}/zh +- Full content of all pages: ${siteUrl}/llms-full.txt +- Markdown source of any page: append \`/raw//.mdx\` to the site URL +- Sitemap: ${siteUrl}/sitemap.xml + +For each page, prefer the canonical URL that matches the reader's language. +When in doubt, link to the English version. + +--- + +`; + export function GET() { - return new Response(llms(source).index()); + const index = llms(source).index(); + const body = `${HEADER}${index}`; + + return new Response(body, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "public, max-age=3600", + }, + }); } From 30c687ac1ae824db63d0b8ac2849609bb813cea3 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 02:28:17 +0800 Subject: [PATCH 32/37] docs: clean up redundant og:title metadata, fix sitemap and OG image configuration 1. Remove redundant og:title fields from all documentation pages, and uniformly generate OG titles using the main title. 2. Update OG image dimensions and links to the official 512x512 icon. 3. Fix the site URL concatenation logic to avoid redundant slashes. 4. Adjust the permission configuration for the GitHub Pages deployment workflow. --- .github/workflows/build-docs.yml | 4 +++- apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx | 10 ++++++---- apps/docs/app/sitemap.ts | 4 ++-- apps/docs/content/internal/developers/architecture.md | 1 - .../docs/content/internal/developers/crawler/meta.json | 3 +-- .../content/internal/developers/crawler/overview.md | 1 - apps/docs/content/internal/developers/guide.md | 1 - apps/docs/content/internal/developers/meta.json | 3 +-- apps/docs/content/internal/developers/style-guide.md | 1 - apps/docs/content/internal/developers/workflow.md | 1 - apps/docs/content/internal/index.md | 1 - .../content/internal/legacy/architecture-overview.md | 1 - .../docs/content/internal/legacy/crawling-lifecycle.md | 1 - .../internal/legacy/database-schema-reference.md | 1 - .../content/internal/legacy/message-queue-reference.md | 1 - apps/docs/content/internal/legacy/meta.json | 1 - .../internal/legacy/network-proxy-architecture.md | 1 - .../docs/content/internal/legacy/snapshot-execution.md | 1 - .../content/internal/legacy/snapshot-scheduling.md | 1 - .../internal/legacy/song-collection-pipeline.md | 1 - .../internal/legacy/video-discovery-pipeline.md | 1 - apps/docs/content/internal/meta.json | 1 - apps/docs/content/not-found.en.md | 3 +-- apps/docs/content/not-found.md | 1 - apps/docs/lib/metadata.ts | 8 ++++---- 25 files changed, 18 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0362c66..596a00e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -11,11 +11,13 @@ concurrency: group: pages-deploy cancel-in-progress: true +jobs: jobs: deploy: runs-on: ubuntu-latest environment: production - + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index 7661edd..d773e8d 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -84,7 +84,9 @@ export async function generateMetadata({ if (!page) notFound(); const cleanSlug = slug ? slug : []; const currentPage = cleanSlug.join("/"); - const url = `${siteUrl}/${lang}/${currentPage}`; + const pagePath = currentPage ? `/${currentPage}` : ""; + + const url = `${siteUrl}/${lang}${currentPage}`; const ogImage = ogImageUrl(); return { title: page.data.title, @@ -92,9 +94,9 @@ export async function generateMetadata({ alternates: { canonical: url, languages: { - zh: `${siteUrl}/zh/${currentPage}`, - en: `${siteUrl}/en/${currentPage}`, - "x-default": `${siteUrl}/en/${currentPage}`, + zh: `${siteUrl}/zh${pagePath}`, + en: `${siteUrl}/en${pagePath}`, + "x-default": `${siteUrl}/en${pagePath}`, }, }, openGraph: { diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts index 72ec61b..f9c2ee9 100644 --- a/apps/docs/app/sitemap.ts +++ b/apps/docs/app/sitemap.ts @@ -11,7 +11,7 @@ export default function sitemap(): MetadataRoute.Sitemap { return { url, - lastModified: new Date(), + // lastModified: new Date(), changeFrequency: "weekly" as const, priority: page.slugs.length === 0 ? 0.8 : 0.6, }; @@ -19,7 +19,7 @@ export default function sitemap(): MetadataRoute.Sitemap { const landingPage: MetadataRoute.Sitemap[number] = { url: siteUrl, - lastModified: new Date(), + // lastModified: new Date(), changeFrequency: "monthly", priority: 1.0, }; diff --git a/apps/docs/content/internal/developers/architecture.md b/apps/docs/content/internal/developers/architecture.md index fb96765..dcc46ba 100644 --- a/apps/docs/content/internal/developers/architecture.md +++ b/apps/docs/content/internal/developers/architecture.md @@ -1,7 +1,6 @@ --- title: 项目架构 description: 了解档案馆的项目组织架构。 -og:title: Project CVSA 项目架构 --- ## 目录结构 diff --git a/apps/docs/content/internal/developers/crawler/meta.json b/apps/docs/content/internal/developers/crawler/meta.json index 3f0762f..fe69897 100644 --- a/apps/docs/content/internal/developers/crawler/meta.json +++ b/apps/docs/content/internal/developers/crawler/meta.json @@ -1,4 +1,3 @@ { - "title": "爬虫架构", - "og:title": "Project CVSA 爬虫架构" + "title": "爬虫架构" } diff --git a/apps/docs/content/internal/developers/crawler/overview.md b/apps/docs/content/internal/developers/crawler/overview.md index b1b9f22..1c3eb35 100644 --- a/apps/docs/content/internal/developers/crawler/overview.md +++ b/apps/docs/content/internal/developers/crawler/overview.md @@ -1,7 +1,6 @@ --- title: 新爬虫架构概览 description: CVSA 2.0 爬虫系统的总体设计、核心设计目标与节点角色定义 -og:title: 新爬虫架构概览 --- ## 设计目标 diff --git a/apps/docs/content/internal/developers/guide.md b/apps/docs/content/internal/developers/guide.md index 82a5690..e20194e 100644 --- a/apps/docs/content/internal/developers/guide.md +++ b/apps/docs/content/internal/developers/guide.md @@ -1,7 +1,6 @@ --- title: 开发者指南 description: 了解如何参与档案馆的开发工作。 -og:title: Project CVSA 开发开发者指南 --- ## 准备环境 diff --git a/apps/docs/content/internal/developers/meta.json b/apps/docs/content/internal/developers/meta.json index 51461ae..7e372a7 100644 --- a/apps/docs/content/internal/developers/meta.json +++ b/apps/docs/content/internal/developers/meta.json @@ -1,4 +1,3 @@ { - "title": "开发文档", - "og:title": "Project CVSA 开发文档" + "title": "开发文档" } diff --git a/apps/docs/content/internal/developers/style-guide.md b/apps/docs/content/internal/developers/style-guide.md index 3d90f9a..ca2b946 100644 --- a/apps/docs/content/internal/developers/style-guide.md +++ b/apps/docs/content/internal/developers/style-guide.md @@ -1,7 +1,6 @@ --- title: 代码风格指南 description: 档案馆项目的代码风格、架构决策及类型安全规范。 -og:title: Project CVSA 代码风格指南 --- ## TypeScript 代码规范 diff --git a/apps/docs/content/internal/developers/workflow.md b/apps/docs/content/internal/developers/workflow.md index 1a72f89..61ed64a 100644 --- a/apps/docs/content/internal/developers/workflow.md +++ b/apps/docs/content/internal/developers/workflow.md @@ -1,7 +1,6 @@ --- title: 流程规范 description: 了解档案馆的开发工作流和相关规范。 -og:title: Project CVSA 流程规范 --- ## 分支命名 diff --git a/apps/docs/content/internal/index.md b/apps/docs/content/internal/index.md index 27db6fe..4e8605a 100644 --- a/apps/docs/content/internal/index.md +++ b/apps/docs/content/internal/index.md @@ -1,7 +1,6 @@ --- title: 成员指南 description: 档案馆成员的内部协作指南。 -og:title: Project CVSA 内部成员协作指南 --- 欢迎加入中 V 档案馆!本指南旨在帮助新成员快速了解项目运作模式、协作规范及工具使用方法。 diff --git a/apps/docs/content/internal/legacy/architecture-overview.md b/apps/docs/content/internal/legacy/architecture-overview.md index fe732fd..5db8d69 100644 --- a/apps/docs/content/internal/legacy/architecture-overview.md +++ b/apps/docs/content/internal/legacy/architecture-overview.md @@ -1,7 +1,6 @@ --- title: 系统架构概览 description: 爬虫系统的整体架构、核心组件与数据流向。 -og:title: Project CVSA 系统架构概览 --- CVSA 的爬虫系统是一个基于 BullMQ 消息队列的数据采集系统,用于持续追踪 Bilibili 视频的播放数据。 diff --git a/apps/docs/content/internal/legacy/crawling-lifecycle.md b/apps/docs/content/internal/legacy/crawling-lifecycle.md index 7137818..ecd1582 100644 --- a/apps/docs/content/internal/legacy/crawling-lifecycle.md +++ b/apps/docs/content/internal/legacy/crawling-lifecycle.md @@ -1,7 +1,6 @@ --- title: 一次完整爬取的生命周期 description: 从视频发现到成就追踪的全流程示例,展示爬虫系统如何处理一个新视频的完整生命周期。 -og:title: 一次完整爬取的生命周期 --- 以一个**新发现的B站视频**(aid=12345)为例,展示从发现到持续追踪的完整流程。 diff --git a/apps/docs/content/internal/legacy/database-schema-reference.md b/apps/docs/content/internal/legacy/database-schema-reference.md index 46e6841..be4f0a6 100644 --- a/apps/docs/content/internal/legacy/database-schema-reference.md +++ b/apps/docs/content/internal/legacy/database-schema-reference.md @@ -1,7 +1,6 @@ --- title: 核心数据表 description: 爬虫系统关键数据表的结构与用途。 -og:title: Project CVSA 核心数据表 --- ## snapshot_schedule diff --git a/apps/docs/content/internal/legacy/message-queue-reference.md b/apps/docs/content/internal/legacy/message-queue-reference.md index 169574e..cdb1812 100644 --- a/apps/docs/content/internal/legacy/message-queue-reference.md +++ b/apps/docs/content/internal/legacy/message-queue-reference.md @@ -1,7 +1,6 @@ --- title: 消息队列与任务定义 description: BullMQ队列配置、定时任务与Job数据结构。 -og:title: Project CVSA 消息队列与任务定义 --- 爬虫系统使用BullMQ作为消息队列,Redis作为底层存储。 diff --git a/apps/docs/content/internal/legacy/meta.json b/apps/docs/content/internal/legacy/meta.json index 7d5dce6..f0a15e8 100644 --- a/apps/docs/content/internal/legacy/meta.json +++ b/apps/docs/content/internal/legacy/meta.json @@ -1,6 +1,5 @@ { "title": "旧版爬虫文档", - "og:title": "Project CVSA 旧版爬虫文档", "pages": [ "architecture-overview", "crawling-lifecycle", diff --git a/apps/docs/content/internal/legacy/network-proxy-architecture.md b/apps/docs/content/internal/legacy/network-proxy-architecture.md index 6fb5bea..31c3564 100644 --- a/apps/docs/content/internal/legacy/network-proxy-architecture.md +++ b/apps/docs/content/internal/legacy/network-proxy-architecture.md @@ -1,7 +1,6 @@ --- title: 网络代理架构 description: 代理类型、请求路由与限速配置。 -og:title: Project CVSA 网络代理架构 --- 网络层负责提供一个网络请求的抽象层,用于将API请求转发到不同的代理后端,并提供限速和流控。 diff --git a/apps/docs/content/internal/legacy/snapshot-execution.md b/apps/docs/content/internal/legacy/snapshot-execution.md index 1e5dddb..281dd7b 100644 --- a/apps/docs/content/internal/legacy/snapshot-execution.md +++ b/apps/docs/content/internal/legacy/snapshot-execution.md @@ -1,7 +1,6 @@ --- title: 快照执行与错误处理 description: 快照任务的执行流程、批量处理与错误恢复机制。 -og:title: Project CVSA 快照执行与错误处理 --- 快照执行系统负责实际从B站获取视频数据并写入数据库。 diff --git a/apps/docs/content/internal/legacy/snapshot-scheduling.md b/apps/docs/content/internal/legacy/snapshot-scheduling.md index 5b5efd1..0f42a2a 100644 --- a/apps/docs/content/internal/legacy/snapshot-scheduling.md +++ b/apps/docs/content/internal/legacy/snapshot-scheduling.md @@ -1,7 +1,6 @@ --- title: 快照调度系统 description: 快照调度的三种类型、时间计算策略与流量控制机制。 -og:title: Project CVSA 快照调度系统 --- 快照调度系统负责决定何时对哪些视频执行数据采集,核心数据存储于 `snapshot_schedule` 表。 diff --git a/apps/docs/content/internal/legacy/song-collection-pipeline.md b/apps/docs/content/internal/legacy/song-collection-pipeline.md index e66aada..87cfca7 100644 --- a/apps/docs/content/internal/legacy/song-collection-pipeline.md +++ b/apps/docs/content/internal/legacy/song-collection-pipeline.md @@ -1,7 +1,6 @@ --- title: 歌曲收集流程 description: 将ML分类为歌曲的视频收集到songs表并触发追踪。 -og:title: Project CVSA 歌曲收集流程 --- 歌曲收集流程负责将ML分类为歌曲的视频正式纳入歌曲库,并开始持续的数据追踪。 diff --git a/apps/docs/content/internal/legacy/video-discovery-pipeline.md b/apps/docs/content/internal/legacy/video-discovery-pipeline.md index 323eb7f..08d8c7a 100644 --- a/apps/docs/content/internal/legacy/video-discovery-pipeline.md +++ b/apps/docs/content/internal/legacy/video-discovery-pipeline.md @@ -1,7 +1,6 @@ --- title: 视频发现与元数据采集 description: 从B站发现新视频、采集元数据并触发分类的流程。 -og:title: Project CVSA 视频发现与元数据采集 --- 视频发现流程负责从Bilibili获取最新视频,采集元数据,并触发后续的分类和追踪。 diff --git a/apps/docs/content/internal/meta.json b/apps/docs/content/internal/meta.json index 385ee48..a4af6b5 100644 --- a/apps/docs/content/internal/meta.json +++ b/apps/docs/content/internal/meta.json @@ -1,6 +1,5 @@ { "title": "内部文档", - "og:title": "Project CVSA 内部文档", "description": "供档案馆团队成员使用的内部文档", "root": true } diff --git a/apps/docs/content/not-found.en.md b/apps/docs/content/not-found.en.md index 0092488..ac19705 100644 --- a/apps/docs/content/not-found.en.md +++ b/apps/docs/content/not-found.en.md @@ -1,6 +1,5 @@ --- -title: Oops! We can’t find the page you’re looking for. -og:title: Oops! Page Not Found +title: Oops! We can't find the page you're looking for. --- It seems the link is broken, or the page has been moved to a new location. diff --git a/apps/docs/content/not-found.md b/apps/docs/content/not-found.md index 5a13d4b..2fb3d1c 100644 --- a/apps/docs/content/not-found.md +++ b/apps/docs/content/not-found.md @@ -1,6 +1,5 @@ --- title: 抱歉,您访问的页面不存在。 -og:title: 页面不存在 --- 看起来我们要找的内容暂时“走丢”了,或者该链接已经失效。 diff --git a/apps/docs/lib/metadata.ts b/apps/docs/lib/metadata.ts index 76dda59..fd4dce5 100644 --- a/apps/docs/lib/metadata.ts +++ b/apps/docs/lib/metadata.ts @@ -7,14 +7,14 @@ export const SITE_TITLE = "Project CVSA | Archive for a Better Future"; export const SITE_DESCRIPTION = "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community."; -/** OG image dimensions (android-chrome-192x192.png) */ -export const OG_IMAGE_SIZE = 192; +/** OG image dimensions (android-chrome-512x512.png) */ +export const OG_IMAGE_SIZE = 512; /** * Returns the full OG image URL. - * Uses the static android-chrome-192x192.png icon. + * Uses the static android-chrome-512x512.png icon. * No hardcoded URLs — uses siteUrl from env. */ export function ogImageUrl(): string { - return `${siteUrl}/android-chrome-192x192.png`; + return `${siteUrl}/android-chrome-512x512.png`; } \ No newline at end of file From 432702c29e20dfa6c21fae8801ba883d4da4dc54 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 02:50:41 +0800 Subject: [PATCH 33/37] refactor(metadata): unify 404 page metadata configuration and fix URL generation logic 1. Add SITE_TITLE_404 and SITE_DESCRIPTION_404 constants to centrally manage 404 page metadata. 2. Fix the path concatenation issue in the URL generation for documentation pages. 3. Refactor the 404 page metadata to use the new constants instead of hardcoded content. 4. Export the SITE_URL constant to reuse the site address configuration. --- apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx | 2 +- apps/docs/app/not-found.tsx | 16 ++++++++-------- apps/docs/lib/metadata.ts | 7 +++++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index d773e8d..7a02d08 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -86,7 +86,7 @@ export async function generateMetadata({ const currentPage = cleanSlug.join("/"); const pagePath = currentPage ? `/${currentPage}` : ""; - const url = `${siteUrl}/${lang}${currentPage}`; + const url = `${siteUrl}/${lang}${pagePath}`; const ogImage = ogImageUrl(); return { title: page.data.title, diff --git a/apps/docs/app/not-found.tsx b/apps/docs/app/not-found.tsx index ad5aa7a..cfdecc2 100644 --- a/apps/docs/app/not-found.tsx +++ b/apps/docs/app/not-found.tsx @@ -2,32 +2,32 @@ import "./global.css"; import { Inter } from "next/font/google"; import { UpdateLanguage } from "@/components/update-language"; import type { Metadata } from "next"; -import { SITE_TITLE, ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; +import { SITE_TITLE_404, SITE_DESCRIPTION_404, ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; const inter = Inter({ subsets: ["latin"], }); export const metadata: Metadata = { - title: `Not Found - ${SITE_TITLE}`, - description: "The page you are looking for does not exist.", + title: `${SITE_TITLE_404}`, + description: SITE_DESCRIPTION_404, robots: { index: false, follow: false }, openGraph: { - title: `Not Found - ${SITE_TITLE}`, - description: "The page you are looking for does not exist.", + title: `${SITE_TITLE_404}`, + description: SITE_DESCRIPTION_404, images: [ { url: ogImageUrl(), width: OG_IMAGE_SIZE, height: OG_IMAGE_SIZE, - alt: SITE_TITLE, + alt: SITE_TITLE_404, }, ], }, // twitter: { // card: "summary_large_image", - // title: `Not Found - ${SITE_TITLE}`, - // description: "The page you are looking for does not exist.", + // title: `Not Found - ${SITE_TITLE_404}`, + // description: SITE_DESCRIPTION_404, // images: [ogImageUrl()], // }, }; diff --git a/apps/docs/lib/metadata.ts b/apps/docs/lib/metadata.ts index fd4dce5..9ef61a9 100644 --- a/apps/docs/lib/metadata.ts +++ b/apps/docs/lib/metadata.ts @@ -2,10 +2,13 @@ import { siteUrl } from "./env"; /** Shared title used across OG tags */ export const SITE_TITLE = "Project CVSA | Archive for a Better Future"; +export const SITE_TITLE_404 = "Not Found"; /** Shared description used across OG tags */ -export const SITE_DESCRIPTION = - "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community."; +export const SITE_DESCRIPTION = "Project CVSA is an archive program aiming to collect and preserve all information about the Chinese singing voice synthesis community."; +export const SITE_DESCRIPTION_404 = "The page you are looking for does not exist."; + +export const SITE_URL = siteUrl; /** OG image dimensions (android-chrome-512x512.png) */ export const OG_IMAGE_SIZE = 512; From 79bd35292c0ce8462ffff1d839eb8b40f899126e Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 03:46:31 +0800 Subject: [PATCH 34/37] Revert "Merge remote-tracking branch 'origin/develop' into update/aibeto-26-SEO_extend_2" This reverts commit c453a838ec7259f295c55e49cb3e3095eed1e251, reversing changes made to 30c687ac1ae824db63d0b8ac2849609bb813cea3. --- .agents/skills/jujutsu-vcs/SKILL.md | 191 --- .env.docker.example | 7 - .github/workflows/ci.yml | 43 - .gitignore | 3 - .opencode/commands/commit.md | 9 - .tokeignore | 1 + .trae/rules/git-commit-message.md | 15 - .vscode/settings.json | 24 - .zed/settings.json | 72 -- AGENTS.md | 114 -- README.md | 10 - apps/backend/.env.example | 8 +- apps/backend/biome.json | 21 - apps/backend/bunfig.toml | 15 - apps/backend/config/no-elysia-typebox.grit | 36 - apps/backend/config/no-try-in-handler.grit | 33 - apps/backend/config/no-zod-any.grit | 9 - apps/backend/package.json | 44 - .../src/common/error/RateLimitError.ts | 7 - .../src/common/error/getErrorResponse.ts | 24 - apps/backend/src/common/error/index.ts | 2 - apps/backend/src/common/i18n.ts | 19 - apps/backend/src/common/log.ts | 50 - apps/backend/src/common/trace.ts | 28 - apps/backend/src/errorHandler.ts | 146 --- apps/backend/src/handlers/auth/currentUser.ts | 32 - apps/backend/src/handlers/auth/index.ts | 11 - apps/backend/src/handlers/auth/login.ts | 62 - apps/backend/src/handlers/auth/logout.ts | 35 - apps/backend/src/handlers/auth/signup.ts | 60 - .../src/handlers/catalog/artist/create.ts | 32 - .../src/handlers/catalog/artist/delete.ts | 34 - .../src/handlers/catalog/artist/get.ts | 29 - .../src/handlers/catalog/artist/index.ts | 13 - .../src/handlers/catalog/artist/search.ts | 25 - .../src/handlers/catalog/artist/update.ts | 39 - .../src/handlers/catalog/artistRole/create.ts | 33 - .../src/handlers/catalog/artistRole/delete.ts | 34 - .../src/handlers/catalog/artistRole/get.ts | 32 - .../src/handlers/catalog/artistRole/index.ts | 13 - .../src/handlers/catalog/artistRole/search.ts | 25 - .../src/handlers/catalog/artistRole/update.ts | 39 - .../src/handlers/catalog/engine/create.ts | 33 - .../src/handlers/catalog/engine/delete.ts | 34 - .../src/handlers/catalog/engine/details.ts | 29 - .../src/handlers/catalog/engine/index.ts | 11 - .../src/handlers/catalog/engine/update.ts | 39 - .../src/handlers/catalog/singer/create.ts | 32 - .../src/handlers/catalog/singer/delete.ts | 34 - .../src/handlers/catalog/singer/get.ts | 29 - .../src/handlers/catalog/singer/index.ts | 13 - .../src/handlers/catalog/singer/search.ts | 25 - .../src/handlers/catalog/singer/update.ts | 39 - .../src/handlers/catalog/song/create.ts | 30 - .../src/handlers/catalog/song/delete.ts | 34 - .../src/handlers/catalog/song/details.ts | 29 - .../src/handlers/catalog/song/index.ts | 15 - .../handlers/catalog/song/lyrics/create.ts | 38 - .../handlers/catalog/song/lyrics/delete.ts | 34 - .../src/handlers/catalog/song/lyrics/get.ts | 29 - .../src/handlers/catalog/song/lyrics/index.ts | 13 - .../src/handlers/catalog/song/lyrics/list.ts | 28 - .../handlers/catalog/song/lyrics/update.ts | 39 - .../src/handlers/catalog/song/search.ts | 25 - .../src/handlers/catalog/song/update.ts | 39 - apps/backend/src/handlers/dev/gc.ts | 12 - apps/backend/src/handlers/dev/index.ts | 7 - apps/backend/src/handlers/dev/snapshot.ts | 13 - apps/backend/src/handlers/index.ts | 7 - apps/backend/src/index.ts | 69 -- apps/backend/src/middlewares/auth.ts | 21 - apps/backend/src/middlewares/i18n.ts | 42 - apps/backend/src/middlewares/index.ts | 3 - apps/backend/src/middlewares/requestLogger.ts | 56 - apps/backend/src/onAfterHandle.ts | 38 - apps/backend/src/startMessage.ts | 4 - apps/backend/tests/artist.test.ts | 325 ----- apps/backend/tests/artistRole.test.ts | 257 ---- apps/backend/tests/auth.test.ts | 275 ----- apps/backend/tests/engine.test.ts | 224 ---- apps/backend/tests/singer.test.ts | 321 ----- apps/backend/tests/song.test.ts | 239 ---- apps/backend/tests/songLyrics.test.ts | 548 --------- apps/backend/tsconfig.json | 13 - apps/backend/turbo.json | 25 - biome.json | 57 - bun.lock | 1043 +---------------- docker-compose.yml | 39 - locale/manifest.json | 4 - locale/modules/backend/manifest.json | 4 - locale/modules/backend/messages/en.json | 15 - locale/modules/backend/messages/zh.json | 15 - opencode.json | 14 - package.json | 29 - packages/core/.env.example | 3 - packages/core/bunfig.toml | 15 - packages/core/package.json | 42 - packages/core/scripts/cleanUpSearchIndex.ts | 13 - packages/core/src/error/AppError.ts | 28 - packages/core/src/error/index.ts | 2 - packages/core/src/error/schema.ts | 25 - packages/core/src/index.ts | 6 - packages/core/src/internal.ts | 5 - packages/core/src/modules/auth/betterAuth.ts | 65 - packages/core/src/modules/auth/index.ts | 2 - packages/core/src/modules/auth/user/dto.ts | 154 --- packages/core/src/modules/auth/user/index.ts | 1 - .../src/modules/catalog/artist/container.ts | 17 - .../core/src/modules/catalog/artist/dto.ts | 31 - .../core/src/modules/catalog/artist/index.ts | 5 - .../catalog/artist/repository.interface.ts | 23 - .../src/modules/catalog/artist/repository.ts | 67 -- .../src/modules/catalog/artist/service.ts | 89 -- .../modules/catalog/artistRole/container.ts | 17 - .../src/modules/catalog/artistRole/dto.ts | 27 - .../src/modules/catalog/artistRole/index.ts | 5 - .../artistRole/repository.interface.ts | 29 - .../modules/catalog/artistRole/repository.ts | 70 -- .../src/modules/catalog/artistRole/service.ts | 92 -- .../src/modules/catalog/engine/container.ts | 6 - .../core/src/modules/catalog/engine/dto.ts | 25 - .../core/src/modules/catalog/engine/index.ts | 5 - .../catalog/engine/repository.interface.ts | 23 - .../src/modules/catalog/engine/repository.ts | 67 -- .../src/modules/catalog/engine/service.ts | 51 - packages/core/src/modules/catalog/index.ts | 5 - .../src/modules/catalog/singer/container.ts | 17 - .../core/src/modules/catalog/singer/dto.ts | 31 - .../core/src/modules/catalog/singer/index.ts | 5 - .../catalog/singer/repository.interface.ts | 23 - .../src/modules/catalog/singer/repository.ts | 67 -- .../src/modules/catalog/singer/service.ts | 89 -- .../src/modules/catalog/song/container.ts | 17 - packages/core/src/modules/catalog/song/dto.ts | 104 -- .../core/src/modules/catalog/song/index.ts | 5 - .../catalog/song/repository.interface.ts | 38 - .../src/modules/catalog/song/repository.ts | 198 ---- .../core/src/modules/catalog/song/service.ts | 194 --- packages/core/src/modules/index.ts | 3 - packages/core/src/modules/outbox/container.ts | 6 - packages/core/src/modules/outbox/dto.ts | 35 - packages/core/src/modules/outbox/index.ts | 5 - .../modules/outbox/repository.interface.ts | 15 - .../core/src/modules/outbox/repository.ts | 71 -- packages/core/src/modules/outbox/service.ts | 78 -- packages/core/src/outbox/index.ts | 2 - packages/core/src/outbox/processor.ts | 55 - packages/core/src/outbox/queue.ts | 62 - packages/core/src/search/catalog/artist.ts | 40 - .../core/src/search/catalog/artistRole.ts | 33 - packages/core/src/search/catalog/index.ts | 4 - packages/core/src/search/catalog/singer.ts | 39 - packages/core/src/search/catalog/song.ts | 77 -- packages/core/src/search/config.ts | 80 -- packages/core/src/search/container.ts | 3 - packages/core/src/search/index.ts | 3 - packages/core/src/search/interface.ts | 105 -- packages/core/src/search/manager.ts | 256 ---- packages/core/src/types/index.ts | 2 - packages/core/src/types/repository.ts | 5 - packages/core/src/types/service.ts | 3 - packages/core/src/utils/BaseRepository.ts | 8 - packages/core/src/utils/deepEqualUnordered.ts | 37 - packages/core/src/utils/index.ts | 3 - packages/core/src/utils/randomId.ts | 4 - .../integration/ArtistRepository.test.ts | 154 --- .../integration/ArtistRoleRepository.test.ts | 127 -- .../integration/EngineRepository.test.ts | 142 --- .../integration/OutboxRepository.test.ts | 322 ----- .../integration/SingerRepository.test.ts | 153 --- .../tests/integration/SongRepository.test.ts | 357 ------ .../integration/SongSearchService.test.ts | 142 --- .../core/tests/unit/ArtistRoleService.test.ts | 178 --- .../tests/unit/ArtistSearchService.test.ts | 241 ---- .../core/tests/unit/ArtistService.test.ts | 183 --- .../core/tests/unit/EngineService.test.ts | 120 -- .../core/tests/unit/OutboxProcessor.test.ts | 116 -- .../core/tests/unit/OutboxService.test.ts | 248 ---- .../core/tests/unit/SearchManager.test.ts | 482 -------- .../tests/unit/SingerSearchService.test.ts | 240 ---- .../core/tests/unit/SingerService.test.ts | 182 --- .../core/tests/unit/SongSearchService.test.ts | 424 ------- packages/core/tests/unit/SongService.test.ts | 353 ------ packages/core/tests/utils.ts | 15 - packages/core/tsconfig.json | 4 - packages/core/turbo.json | 25 - packages/db/.env.example | 6 +- packages/db/README.md | 4 - packages/db/package.json | 31 - .../20260227101459_init/migration.sql | 514 ++++++++ .../db/prisma/migrations/migration_lock.toml | 3 + packages/db/prisma/models/auth/account.prisma | 20 - packages/db/prisma/models/auth/session.prisma | 15 - packages/db/prisma/models/auth/user.prisma | 11 +- .../db/prisma/models/auth/verification.prisma | 12 - packages/db/prisma/models/core/album.prisma | 1 - packages/db/prisma/models/core/artist.prisma | 18 - .../db/prisma/models/core/artistRole.prisma | 12 - packages/db/prisma/models/core/lyrics.prisma | 14 - packages/db/prisma/models/core/singer.prisma | 17 - packages/db/prisma/models/core/song.prisma | 14 +- .../db/prisma/models/core/songSeries.prisma | 1 - .../db/prisma/models/core/svsEngine.prisma | 3 +- .../models/core/svsEngineVersion.prisma | 12 - .../db/prisma/models/core/voicebank.prisma | 2 +- packages/db/prisma/models/meta/outbox.prisma | 27 - .../db/prisma/relations/core/creation.prisma | 15 - .../prisma/relations/core/performance.prisma | 22 - .../prisma/relations/core/singerOfSong.prisma | 6 +- .../relations/core/singerSvsEngine.prisma | 13 + .../core/singerSvsEngineVersion.prisma | 13 + packages/db/prisma/schema.prisma | 20 +- packages/db/prisma/types.ts | 11 - packages/db/prisma/zodSchema.ts | 3 - packages/db/src/index.ts | 9 - packages/db/src/prisma.ts | 58 - packages/db/src/types.ts | 5 - packages/db/tsconfig.json | 4 - packages/embedding/package.json | 24 - packages/embedding/src/embedding.ts | 166 --- packages/embedding/src/index.ts | 35 - packages/embedding/tests/e2e.test.ts | 18 - packages/embedding/tsconfig.json | 4 - packages/env/package.json | 18 - packages/env/src/index.ts | 22 - packages/env/tsconfig.json | 4 - packages/logger/package.json | 22 - packages/logger/src/appLogger.ts | 140 --- packages/logger/src/index.ts | 11 - packages/logger/src/pinoSetup.ts | 47 - packages/logger/src/types.ts | 52 - packages/logger/tsconfig.json | 4 - packages/observability/package.json | 21 - packages/observability/src/index.ts | 2 - packages/observability/src/init.ts | 20 - packages/observability/src/tracer.ts | 18 - packages/observability/tsconfig.json | 4 - packages/typescript-config/base.json | 12 - packages/typescript-config/package.json | 4 - scripts/import.ts | 195 --- scripts/tsconfig.json | 16 - turbo.json | 63 - 242 files changed, 566 insertions(+), 14186 deletions(-) delete mode 100644 .agents/skills/jujutsu-vcs/SKILL.md delete mode 100644 .env.docker.example delete mode 100644 .github/workflows/ci.yml delete mode 100644 .opencode/commands/commit.md delete mode 100644 .vscode/settings.json delete mode 100644 .zed/settings.json delete mode 100644 AGENTS.md delete mode 100644 apps/backend/biome.json delete mode 100644 apps/backend/bunfig.toml delete mode 100644 apps/backend/config/no-elysia-typebox.grit delete mode 100644 apps/backend/config/no-try-in-handler.grit delete mode 100644 apps/backend/config/no-zod-any.grit delete mode 100644 apps/backend/src/common/error/RateLimitError.ts delete mode 100644 apps/backend/src/common/error/getErrorResponse.ts delete mode 100644 apps/backend/src/common/error/index.ts delete mode 100644 apps/backend/src/common/i18n.ts delete mode 100644 apps/backend/src/common/log.ts delete mode 100644 apps/backend/src/common/trace.ts delete mode 100644 apps/backend/src/errorHandler.ts delete mode 100644 apps/backend/src/handlers/auth/currentUser.ts delete mode 100644 apps/backend/src/handlers/auth/index.ts delete mode 100644 apps/backend/src/handlers/auth/login.ts delete mode 100644 apps/backend/src/handlers/auth/logout.ts delete mode 100644 apps/backend/src/handlers/auth/signup.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/create.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/get.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/index.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/search.ts delete mode 100644 apps/backend/src/handlers/catalog/artist/update.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/create.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/get.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/index.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/search.ts delete mode 100644 apps/backend/src/handlers/catalog/artistRole/update.ts delete mode 100644 apps/backend/src/handlers/catalog/engine/create.ts delete mode 100644 apps/backend/src/handlers/catalog/engine/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/engine/details.ts delete mode 100644 apps/backend/src/handlers/catalog/engine/index.ts delete mode 100644 apps/backend/src/handlers/catalog/engine/update.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/create.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/get.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/index.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/search.ts delete mode 100644 apps/backend/src/handlers/catalog/singer/update.ts delete mode 100644 apps/backend/src/handlers/catalog/song/create.ts delete mode 100644 apps/backend/src/handlers/catalog/song/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/song/details.ts delete mode 100644 apps/backend/src/handlers/catalog/song/index.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/create.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/delete.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/get.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/index.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/list.ts delete mode 100644 apps/backend/src/handlers/catalog/song/lyrics/update.ts delete mode 100644 apps/backend/src/handlers/catalog/song/search.ts delete mode 100644 apps/backend/src/handlers/catalog/song/update.ts delete mode 100644 apps/backend/src/handlers/dev/gc.ts delete mode 100644 apps/backend/src/handlers/dev/index.ts delete mode 100644 apps/backend/src/handlers/dev/snapshot.ts delete mode 100644 apps/backend/src/handlers/index.ts delete mode 100644 apps/backend/src/middlewares/auth.ts delete mode 100644 apps/backend/src/middlewares/i18n.ts delete mode 100644 apps/backend/src/middlewares/index.ts delete mode 100644 apps/backend/src/middlewares/requestLogger.ts delete mode 100644 apps/backend/tests/artist.test.ts delete mode 100644 apps/backend/tests/artistRole.test.ts delete mode 100644 apps/backend/tests/auth.test.ts delete mode 100644 apps/backend/tests/engine.test.ts delete mode 100644 apps/backend/tests/singer.test.ts delete mode 100644 apps/backend/tests/song.test.ts delete mode 100644 apps/backend/tests/songLyrics.test.ts delete mode 100644 apps/backend/turbo.json delete mode 100644 docker-compose.yml delete mode 100644 locale/manifest.json delete mode 100644 locale/modules/backend/manifest.json delete mode 100644 locale/modules/backend/messages/en.json delete mode 100644 locale/modules/backend/messages/zh.json delete mode 100644 opencode.json delete mode 100644 packages/core/.env.example delete mode 100644 packages/core/bunfig.toml delete mode 100644 packages/core/package.json delete mode 100644 packages/core/scripts/cleanUpSearchIndex.ts delete mode 100644 packages/core/src/error/AppError.ts delete mode 100644 packages/core/src/error/index.ts delete mode 100644 packages/core/src/error/schema.ts delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/internal.ts delete mode 100644 packages/core/src/modules/auth/betterAuth.ts delete mode 100644 packages/core/src/modules/auth/index.ts delete mode 100644 packages/core/src/modules/auth/user/dto.ts delete mode 100644 packages/core/src/modules/auth/user/index.ts delete mode 100644 packages/core/src/modules/catalog/artist/container.ts delete mode 100644 packages/core/src/modules/catalog/artist/dto.ts delete mode 100644 packages/core/src/modules/catalog/artist/index.ts delete mode 100644 packages/core/src/modules/catalog/artist/repository.interface.ts delete mode 100644 packages/core/src/modules/catalog/artist/repository.ts delete mode 100644 packages/core/src/modules/catalog/artist/service.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/container.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/dto.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/index.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/repository.interface.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/repository.ts delete mode 100644 packages/core/src/modules/catalog/artistRole/service.ts delete mode 100644 packages/core/src/modules/catalog/engine/container.ts delete mode 100644 packages/core/src/modules/catalog/engine/dto.ts delete mode 100644 packages/core/src/modules/catalog/engine/index.ts delete mode 100644 packages/core/src/modules/catalog/engine/repository.interface.ts delete mode 100644 packages/core/src/modules/catalog/engine/repository.ts delete mode 100644 packages/core/src/modules/catalog/engine/service.ts delete mode 100644 packages/core/src/modules/catalog/index.ts delete mode 100644 packages/core/src/modules/catalog/singer/container.ts delete mode 100644 packages/core/src/modules/catalog/singer/dto.ts delete mode 100644 packages/core/src/modules/catalog/singer/index.ts delete mode 100644 packages/core/src/modules/catalog/singer/repository.interface.ts delete mode 100644 packages/core/src/modules/catalog/singer/repository.ts delete mode 100644 packages/core/src/modules/catalog/singer/service.ts delete mode 100644 packages/core/src/modules/catalog/song/container.ts delete mode 100644 packages/core/src/modules/catalog/song/dto.ts delete mode 100644 packages/core/src/modules/catalog/song/index.ts delete mode 100644 packages/core/src/modules/catalog/song/repository.interface.ts delete mode 100644 packages/core/src/modules/catalog/song/repository.ts delete mode 100644 packages/core/src/modules/catalog/song/service.ts delete mode 100644 packages/core/src/modules/index.ts delete mode 100644 packages/core/src/modules/outbox/container.ts delete mode 100644 packages/core/src/modules/outbox/dto.ts delete mode 100644 packages/core/src/modules/outbox/index.ts delete mode 100644 packages/core/src/modules/outbox/repository.interface.ts delete mode 100644 packages/core/src/modules/outbox/repository.ts delete mode 100644 packages/core/src/modules/outbox/service.ts delete mode 100644 packages/core/src/outbox/index.ts delete mode 100644 packages/core/src/outbox/processor.ts delete mode 100644 packages/core/src/outbox/queue.ts delete mode 100644 packages/core/src/search/catalog/artist.ts delete mode 100644 packages/core/src/search/catalog/artistRole.ts delete mode 100644 packages/core/src/search/catalog/index.ts delete mode 100644 packages/core/src/search/catalog/singer.ts delete mode 100644 packages/core/src/search/catalog/song.ts delete mode 100644 packages/core/src/search/config.ts delete mode 100644 packages/core/src/search/container.ts delete mode 100644 packages/core/src/search/index.ts delete mode 100644 packages/core/src/search/interface.ts delete mode 100644 packages/core/src/search/manager.ts delete mode 100644 packages/core/src/types/index.ts delete mode 100644 packages/core/src/types/repository.ts delete mode 100644 packages/core/src/types/service.ts delete mode 100644 packages/core/src/utils/BaseRepository.ts delete mode 100644 packages/core/src/utils/deepEqualUnordered.ts delete mode 100644 packages/core/src/utils/index.ts delete mode 100644 packages/core/src/utils/randomId.ts delete mode 100644 packages/core/tests/integration/ArtistRepository.test.ts delete mode 100644 packages/core/tests/integration/ArtistRoleRepository.test.ts delete mode 100644 packages/core/tests/integration/EngineRepository.test.ts delete mode 100644 packages/core/tests/integration/OutboxRepository.test.ts delete mode 100644 packages/core/tests/integration/SingerRepository.test.ts delete mode 100644 packages/core/tests/integration/SongRepository.test.ts delete mode 100644 packages/core/tests/integration/SongSearchService.test.ts delete mode 100644 packages/core/tests/unit/ArtistRoleService.test.ts delete mode 100644 packages/core/tests/unit/ArtistSearchService.test.ts delete mode 100644 packages/core/tests/unit/ArtistService.test.ts delete mode 100644 packages/core/tests/unit/EngineService.test.ts delete mode 100644 packages/core/tests/unit/OutboxProcessor.test.ts delete mode 100644 packages/core/tests/unit/OutboxService.test.ts delete mode 100644 packages/core/tests/unit/SearchManager.test.ts delete mode 100644 packages/core/tests/unit/SingerSearchService.test.ts delete mode 100644 packages/core/tests/unit/SingerService.test.ts delete mode 100644 packages/core/tests/unit/SongSearchService.test.ts delete mode 100644 packages/core/tests/unit/SongService.test.ts delete mode 100644 packages/core/tests/utils.ts delete mode 100644 packages/core/tsconfig.json delete mode 100644 packages/core/turbo.json create mode 100644 packages/db/prisma/migrations/20260227101459_init/migration.sql create mode 100644 packages/db/prisma/migrations/migration_lock.toml delete mode 100644 packages/db/prisma/models/auth/account.prisma delete mode 100644 packages/db/prisma/models/auth/verification.prisma delete mode 100644 packages/db/prisma/models/meta/outbox.prisma delete mode 100644 packages/db/prisma/relations/core/creation.prisma delete mode 100644 packages/db/prisma/relations/core/performance.prisma create mode 100644 packages/db/prisma/relations/core/singerSvsEngine.prisma create mode 100644 packages/db/prisma/relations/core/singerSvsEngineVersion.prisma delete mode 100644 packages/db/prisma/zodSchema.ts delete mode 100644 packages/db/src/index.ts delete mode 100644 packages/db/src/prisma.ts delete mode 100644 packages/db/src/types.ts delete mode 100644 packages/db/tsconfig.json delete mode 100644 packages/embedding/package.json delete mode 100644 packages/embedding/src/embedding.ts delete mode 100644 packages/embedding/src/index.ts delete mode 100644 packages/embedding/tests/e2e.test.ts delete mode 100644 packages/embedding/tsconfig.json delete mode 100644 packages/env/package.json delete mode 100644 packages/env/src/index.ts delete mode 100644 packages/env/tsconfig.json delete mode 100644 packages/logger/package.json delete mode 100644 packages/logger/src/appLogger.ts delete mode 100644 packages/logger/src/index.ts delete mode 100644 packages/logger/src/pinoSetup.ts delete mode 100644 packages/logger/src/types.ts delete mode 100644 packages/logger/tsconfig.json delete mode 100644 packages/observability/package.json delete mode 100644 packages/observability/src/index.ts delete mode 100644 packages/observability/src/init.ts delete mode 100644 packages/observability/src/tracer.ts delete mode 100644 packages/observability/tsconfig.json delete mode 100644 packages/typescript-config/base.json delete mode 100644 packages/typescript-config/package.json delete mode 100644 scripts/import.ts delete mode 100644 scripts/tsconfig.json diff --git a/.agents/skills/jujutsu-vcs/SKILL.md b/.agents/skills/jujutsu-vcs/SKILL.md deleted file mode 100644 index 19e65c5..0000000 --- a/.agents/skills/jujutsu-vcs/SKILL.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -name: jujutsu-vcs -description: The guide of using Jujutsu instead of Git for version control. Always activate FIRST on any git/VCS operations (commit, status, branch, push, etc.). DO NOT IGNORE. -allowed-tools: Bash(jj *) ---- - -This skill helps you work with Jujutsu, a Git-compatible VCS featuring a simpler workflow, a more user-friendly CLI, and a lower risk of data loss. -This skill does not include instructions on workflow; it only introduces jj and teaches you how to use it. You should follow the instructions provided by the user, or the workflow preferences indicated elsewhere in the context. -By default, jj use Git as its storage backend. This means commits, trees, blobs, and file contents are stored in a regular Git repository for full compatibility with Git remotes, hosting platforms, and existing Git tools, while jj-specific metadata—like bookmarks, the operation log, and change IDs—is kept separately in the .jj/ directory. - -## Identifing JJ Availability - -The `.jj` dir excludes itself by `.gitignore` and does not share across the team. -This means you need to separately check whether JJ is available on the user’s computer. -Sometimes, the agent tool may not list `.jj` when you try to list files or dirs in the project because it’s in `.gitignore`. -Hence, running `jj` command is always the most reliable way to determine whether this is a jj repository. -If you got output like `Error: There is no jj repo in "."` or "command not found", this is not a jj repo. - -## Key concepts - -- **Change**: A logical unit of work with a stable identifier. It represents ongoing development of a feature, fix, or piece of functionality -- **Change ID**: A unique identifier for a change. You can refer them in a short form of few letters in k-z range (e.g. `uvzonqtw`) -- **Bookmark**: A bookmark is a named pointer to a commit. They are similar to Git’s branches and even more similar to Mercurial’s bookmarks. Unlike in Git, there is no concept of a "current bookmark"; bookmarks do not move when you create a new commit -- **Commit**: A snapshot of the files in the repository at a given point in time (technically a tree object), together with some metadata. The word "revision" is used as a synonym for "commit" -- **Working Copy**: Contains the files you’re currently working on. It is automatically snapshot at the beginning of almost every jj command, thus creating a new working-copy commit if any changes had been made in the working copy -- **Revsets**: An expression for selecting a set of commits -- **Filesets**: An expression for selecting a set of file - -## Filesets and Revsets - -In cheatsheet below, `[paths]` follows filesets syntax, and symbols like `@`, `x` and `y` can be replaced with any string that follows revsets syntax. -Note that when using filesets and revsets in command, you may need to enclose them in quotation marks to prevent them being accidentally escaped. - -### Revsets Syntax - -> Hint: Run `jj help -k revsets` to learn more - -`@`: working copy, and `@-` for its parent -Tag name or bookmark name like `v3.1.4` or `feat/my-feature` -ID like `uvzonqtw` means a specific commit -`x-`: parents of x -`x+`: children of x -`x::y`: descendants of x that are also ancestors of y. Same as git’s `--ancestry-path x..y` - -### Filesets Syntax - -> Hint: Run `jj help -k filesets` to learn more - -By default, the expression `path` is parsed as a glob pattern. - -`file:path`: Matches cwd-relative exact file path. -`glob:pattern`: Matches file paths with cwd-relative Unix-style shell wildcard pattern. -`glob-i:pattern`: Case insensitive glob. -`root:path`, `root-glob:pattern`, `root-file:path`: Similar to above, but relative to the workspace (project root) - -Examples: -`glob:*.c`: all .c files in the current working directory non-recursively. -`glob:**/*.c`: similar to above one, but matches recursively. -`src ~ glob:**/*.rs`: - -### Shared Syntax - -These syntax are available both in revsets and filesets. - -- `~` means not, so `~x` matches everything but `x` and `x ~ y` matches x but not y -- `&` means and, so `x & y` matches intersection of x and y -- `|` means or, so `x | y` matches union of x and y - -## Key differences from Git - -1. **Working Copy as a Commit**: No staging area. Files are automatically tracked and added to the current commit every time you run `jj` commands -2. **Effortless Jumping**: No stash or WIP commits needed. If you want to jump to another branch or commit, just `jj edit` it. There are no "uncommitted changes" blocking you -3. **Anonymous Branches**: Creating a named branch does not necessarily have to be your first step. You can naturally let the commit tree branch without having to name the branches -4. **Operation Log**: Every single action—every rebase, every description change, every file edit—is recorded in the operation log. You can run `jj undo` to instantly revert the last command, no matter how complex it was -5. **First-class Conflicts**: Conflicts are stored in the history. Conflicted states can be further rebased, merged, or backed out. You don’t have to resolve them before you work - -## Cheatsheet - -This cheatsheet is enough for common operations, organized by function. -JJ's help doc is comprehensive and easy to understand. Whenever you stuck at somewhere, you can always run `jj help` or pass the `--help` flag to any command to get help on how to use jj. - -> **Note**: Always use `--git` or `-s` with diff/show commands to prevent GUI clients from hanging. - -```bash -# Basic Operations -jj status # Show status of working copy, including files changed -jj log --limit n # Show recent n commits -jj log --revisions @ --no-graph --template "description" # See description of working copy - -# Diff (always use --git or -s to prevent GUI hang) -# We exclude lock files using filesets syntax -jj diff --git '~*.lock' # Show diff of working copy vs parent -jj diff --git -r x '~*.lock' # Show diff between x and its parent -jj diff --git [paths] # Show diff for specific paths -jj diff --git --from x --to y '~*.lock' # Show diff from x to y -jj diff -s # Show only whether modified, added, or deleted - -# Show commit info (use --summary to prevent GUI hang) -jj show --summary x # Show summary of commit x - -# Creating & Modifying Commits -jj new # Create new change on top of current one -jj new main # Create new change on top of main bookmark -jj edit x # Move working copy to x (edit commit x) -jj describe -m "msg" # Edit commit message of working copy -jj describe -m "msg" -r x # Edit commit message of revision x -jj commit # Finalize working copy as a new commit -jj squash # Fold current change into its parent -jj restore [paths] # Restore paths in working copy to parent commit -jj split [paths] -m "msg" # Split commit: paths stay in older, rest go to newer -jj abandon # Abandon working copy change -jj abandon -r x # Abandon specific revision - -# Rebasing -jj rebase --source @ --onto x # Rebase current commit and descendants onto x -jj rebase --revisions x --insert-after y # Put x right after y - -# Bookmarks (like Git branches) -jj bookmark create my-feature # Create new bookmark -jj bookmark delete my-feature # Delete bookmark -jj bookmark list # List all bookmarks -# Note: Bookmarks don't auto-move on new commits. Update before pushing: -jj bookmark move my-feature --to @ # Move bookmark to working copy - -# File Operations -jj file list -r x # Show all files contained in commit x -jj file list -r x [paths] # Show paths contained in commit x -jj file untrack [paths] # Like `git rm --cached` -jj file annotate # Like `git blame` - -# Git Interop -jj git init [--no-colocate] # Initialize jj repo (optionally non-colocated) -jj git clone # Clone repository -jj git fetch # Fetch from remote (NO direct equivalent to git pull) -jj git push # Push to remote (auto force-push) -jj git push --bookmark develop # Push specific bookmark only - -# Operation Log (powerful undo/redo) -jj undo # Undo last operation -jj redo # Redo most recently undone operation -jj op log --limit n # Show recent n operations -jj op restore id # Restore repo to state of operation id -jj op diff --from x --to y # See what changed from operation x to y -jj op diff --from x --patch --git # See git-style diff since operation x -``` - -### Reading logs - -You can use `jj log` to see the history tree. You’d better use `jj log --limit n` to prevent context overflow. -A typical log looks like this: -``` -@ mwqpzkuz user@example.com 2026-03-12 10:16:57 main e60cfa44 -│ fix: a bug -│ ○ nynptron user@example.com 2026-03-14 15:45:45 develop 36b98762 -├─╯ feat: a new feature under development -◆ rvmwuqwt user@example.com 2026-03-11 12:35:07 git_head() 425d78a6 -│ feat: an amazing feature -``` -In which: -- `mwqpzkuz` is the change ID (consistent), while `e60cfa44` is the commit ID (same as Git’s commit ID). -- ◆ means immutable commit (usually it’s already pushed to the remote) -- @ means the working copy -- ○ means regular commit, you can modify it if you want. -- Ignore `git_head()`, it’s deprecated. - -### Splitting Commit - -You can use `jj split [paths]` command to split one commit into two commits, each contains a portion of the changes. Changes in files specified via `[paths]` will kept in older commit, while the rest will be present in newer commit. -Let’s assume that you want to split the current commit (x) into two commits: x and y (x is the parent of y). -If current commit has two files changed: `foo` and `bar`, and if you want the x contains `foo` and y contains `bar`, you can run: `jj split "foo" -m "commit message for x"`. This results in: - -Before -``` -@ x user@example.com (Files changed: `foo`, `bar`) -| (no description set) -``` -After -``` -@ y user@example.com (Files changed: `bar`) -| (no description set) -○ x user@example.com (Files changed: `foo`) -| commit message for x -``` - -## Additional Notes - -**IMPORTANT**: Read this section to prevent users from been fired! - -Although you can use `jj edit` to modify any commit, jj will prevent you from editing an immutable commit. Commits that have already been pushed to remote are usually immutable. -`jj git push` will automatically use force push, so the only way to prevent remote history from being overwritten is to avoid modifying immutable commits. -You can pass `--ignore-immutable` when jj hints you `Error: Commit x is immutable` to force-modify a commit, but you should only do this when the user explicitly tells you that you may do so. DO NOT pass `--ignore-immutable` without user’s approval. It’s as dangerous as Git force push. diff --git a/.env.docker.example b/.env.docker.example deleted file mode 100644 index ed6bf72..0000000 --- a/.env.docker.example +++ /dev/null @@ -1,7 +0,0 @@ -# PostgreSQL -POSTGRES_USER=cvsa -POSTGRES_PASSWORD=password -POSTGRES_DB=cvsa - -# Meilisearch -MEILI_MASTER_KEY=0712041211241099 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7ee67a0..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CI - -on: - push: - branches: [develop] - pull_request: - branches: [develop] - workflow_dispatch: - -jobs: - check: - name: Test and Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.3.11 - - - name: Prepare .env files - run: | - cp .env.docker.example .env.docker - cp packages/db/.env.example packages/db/.env - cp packages/db/.env.example packages/db/.env.test - cp packages/core/.env.example packages/core/.env.test - cp apps/backend/.env.example apps/backend/.env.test - - - name: Start services with Docker Compose - run: docker compose up -d - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Setup environment - run: bun run setup:ci - - - name: Test - run: bun run test - - - name: Lint - run: bun run ci diff --git a/.gitignore b/.gitignore index 0361df4..f6b67bb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env -.env.docker .env.development.local .env.test.local .env.test @@ -39,6 +38,4 @@ data .turbo -# ML models - model diff --git a/.opencode/commands/commit.md b/.opencode/commands/commit.md deleted file mode 100644 index b88191d..0000000 --- a/.opencode/commands/commit.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Write a commit message ---- - -Use VCS tools to check what has changed in the current commit, then write a commit message. - -The commit message should follow the 50/70 rule and describe both the motivation for the commit and the changes it involves. - -After drafting your first version, present it to the user and modify it if they give you any feedback. diff --git a/.tokeignore b/.tokeignore index fbc5449..ac5f108 100644 --- a/.tokeignore +++ b/.tokeignore @@ -1,3 +1,4 @@ +prisma/ bruno/ *.json *.sql diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md index 02b9447..89ec985 100644 --- a/.trae/rules/git-commit-message.md +++ b/.trae/rules/git-commit-message.md @@ -1,18 +1,9 @@ --- alwaysApply: true scene: git_message -<<<<<<< HEAD language: en_US --- -======= ---- - -## 提交信息 - -项目采用 Conventional Commits 规范来格式化提交信息。 - ->>>>>>> origin/develop ### 基本格式 每条提交信息应遵循以下结构(只允许使用英文): @@ -20,15 +11,9 @@ language: en_US ```text <类型>(<作用域>): <描述> -<<<<<<< HEAD [正文(英文描述)] [脚注(英文描述)] -======= -[正文] - -[脚注] ->>>>>>> origin/develop ``` - **类型**:必填,表示本次提交的类别(见下文“类型列表”)。 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d5440cf..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "editor.defaultFormatter": "biomejs.biome", - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[prisma]": { - "editor.defaultFormatter": "Prisma.prisma" - }, - "[github-actions-workflow]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index b9f8297..0000000 --- a/.zed/settings.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "languages": { - "Astro": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "CSS": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "HTML": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "JSON": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "JSONC": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "JavaScript": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "TSX": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - "TypeScript": { - "formatter": { - "language_server": { - "name": "biome" - } - } - }, - }, - "language_servers": [ - "!eslint", - "!typescript-language-server", - "..." - ], - "lsp": { - "biome": { - "settings": { - "require_config_file": true - } - } - } -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 45cfbf8..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,114 +0,0 @@ -# System Prompt & Guide for AI Agents - -## 1. System Context -**Project:** CVSA (Chinese Vocal Synthesizer Archive) -**GitHub Repository**: [project-cvsa/cvsa](https://github.com/project-cvsa/cvsa) -**Repository Type:** Monorepo (Turborepo) -**Tech Stack:** Bun, Elysia, Prisma, PostgreSQL, Zod, TypeScript, Biome - -### Workspace Structure -- `apps/backend`: REST API (Elysia) -- `apps/frontend`: Web interface (Not yet implemented) -- `packages/db`: Prisma schema and generated database client -- `packages/core`: Shared business logic, DTOs, and Zod schemas -- `packages/embedding`: A standalone embedding REST service -- `packages/logger`: The application logger setup -- `packages/observability`: Observability configurations -- `packages/env`: Package for managing environment variables - -## 2. Core Mental Model & Architecture - -When working on the backend, you MUST adhere to the Layered Architecture: `Handler -> Service -> Repository`. - -### Responsibility Isolation -1. **Handler (`apps/backend`)**: Only handles HTTP requests/responses, validates via Zod schemas, and calls Services. Never write business logic here. -2. **Service (`packages/core`)**: Contains pure business logic. Calls Repositories. Throws `AppError` on failure. -3. **Repository (`packages/core`)**: Implements pure database operations using Prisma. -4. **DTOs (`packages/core`)**: All request/response validation schemas and corresponding types exist here. - -### File Organization Mapping -If building a module named `[module]`, enforce this strict tree: -```text -apps/backend/src/handlers/[module]/ -├── index.ts # Exports handlers -└── [method].ts # Elysia routes - -packages/core/src/modules/[module]/ -├── index.ts # Public API exports -├── dto.ts # Zod schemas (Validation & Types) -├── service.ts # Business logic -├── repository.ts # Prisma DB operations -├── container.ts # DI container -└── repository.interface.ts # DB contract -``` - -## 3. Strict Constraints & Safety - -These rules are absolute. Violation will cause pipeline or runtime failures. - -### Type Safety [CRITICAL] -- **[PROHIBITED]** `any` type: Never use `any`. -- **[PROHIBITED]** Inline type assertions: Do not use `as SomeType`, use type annotations. -- **[PROHIBITED]** `// @ts-ignore` or `// @ts-expect-error` in first-party code. (Only allowed in third-party code if a GitHub issue URL appended). -- **[PROHIBITED]** Defining local interfaces scoped to a single file for API contracts. -- **[REQUIRED]** Import database types directly from `@cvsa/db` (e.g., `Song`, `Artist`). - -### Validation & Zod -- **[PROHIBITED]** `z.any()` in any schema. -- **[PROHIBITED]** `import { t } from "elysia"`. Always use Zod, never Elysia's TypeBox. -- **[PROHIBITED]** Defining Zod schemas inline inside handlers. -- **[REQUIRED]** Define all Zod schemas in `packages/core/modules//dto.ts` first. -- **[REQUIRED]** Use `z.infer` to derive TypeScript types from Zod schemas. - -### Database & Error Handling -- **[PROHIBITED]** Modifying Prisma schema unless explicitly instructed by the user. -- **[PROHIBITED]** Using `try-catch` blocks in Elysia Handlers. -- **[REQUIRED]** Throw `AppError` directly from Services. - -## 4. Coding Standards - -### Naming Conventions -- **PascalCase**: Classes (`UserService`), Interfaces (`SignupRequest`), Types, and Class file names (`AppError.ts`). -- **camelCase**: Functions (`getUserById`), Variables, and standard file names (`authHandler.ts`). -- **UPPER_SNAKE_CASE**: Constants (`MAX_RATE_LIMIT`). - -### Module-Specific Information -- **Backend**: - 1. Our endpoints starts with `/v2` and this prefix is already configured in root handler. In all of the rest handlers, always use the complete path **without** the version prefix (e.g. `/song/:id/details`) - 2. **[PROHIBITED]**: DO NOT use verbs in paths. Endpoints should strictly follows the RESTful pattern. - -## 5. Execution Commands & Testing - -ALWAYS use `bun` as the package manager, test/command runner. - -### Global Monorepo Commands -```bash -bun lint # Check code issues via Biome -bun lint:fix # Auto-fix code issues -bun format # Format code via Biome -bun typecheck # Run tsc across all packages -# IMPORTANT: ALWAYS use `bun run test` -# [PROHIBITED]: Directly run `bun test` without `run` -bun run test # Run all tests globally -bun run test:coverage # Run all tests with coverage -``` - -### Module-Specific Testing Strategy -When writing tests, apply the following methodologies: -- **Backend (`apps/backend`)**: Write End-to-End (E2E) tests using Elysia Eden Treaty. -- **Core Services (`packages/core`)**: Write Unit tests. Mock the repository layer. -- **Core Repositories (`packages/core`)**: Write Integration tests connecting to a test database. - -## 6. Version Control & Collaboration - -### VCS Tooling (jujutsu / git) -- Prefer `jj` (Jujutsu) over `git` if available. Use "jujutsu-vcs" skill before performing VCS operations. -- If `jj` is unavailable, fallback to `git` with extreme caution. - -### Workflow & Branching -1. Branch from `develop`. -2. Format: `/--` (e.g., `fix/jack-132-song-api`). -3. Commit Message Format (Conventional Commits): - `(optional scope): ` - Types allowed: `feat`, `fix`, `update`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `chore`, `revert`. -4. PR Requirements: The PR title should be a commit message format (e.g. `feat(auth): implement forgot password`) that describe all changes within it. The PR description must contain `## Changes` and `## Related` sections. Maintainers will squash & merge. diff --git a/README.md b/README.md index 4983ceb..edf28a9 100644 --- a/README.md +++ b/README.md @@ -57,21 +57,11 @@ cvsa/ 3. **Set up environment variables** -<<<<<<< HEAD Create `.env` files in each service directory following their `.env.example` templates: ```bash cp apps/frontend/.env.example apps/frontend/.env cp packages/db/.env.example packages/db/.env -======= - Create `.env` and `.env.test` files in each service directory following their `.env.example` templates: - - ```bash - cp apps/frontend/.env.example apps/frontend/.env - cp apps/frontend/.env.example apps/frontend/.env.test - cp packages/db/.env.example packages/db/.env - cp packages/core/.env.example packages/core/.env.test ->>>>>>> origin/develop ``` 4. **Initialize the database** diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 58a78f6..701c7b7 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -1,7 +1 @@ -<<<<<<< HEAD -DATABASE_URL=postgres://username:password@localhost:5432/cvsa -======= -DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa -MEILI_MASTER_KEY=0712041211241099 -MEILI_API_URL=http://127.0.0.1:7700 ->>>>>>> origin/develop +DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file diff --git a/apps/backend/biome.json b/apps/backend/biome.json deleted file mode 100644 index 57f794e..0000000 --- a/apps/backend/biome.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", - "root": false, - "extends": "//", - "grit": { - "formatter": { - "enabled": false - }, - "assist": { - "enabled": false - }, - "linter": { - "enabled": false - } - }, - "plugins": [ - "./config/no-elysia-typebox.grit", - "./config/no-try-in-handler.grit", - "./config/no-zod-any.grit" - ] -} diff --git a/apps/backend/bunfig.toml b/apps/backend/bunfig.toml deleted file mode 100644 index 4856355..0000000 --- a/apps/backend/bunfig.toml +++ /dev/null @@ -1,15 +0,0 @@ -[test] -coveragePathIgnorePatterns = [ - "**/db/prisma/**", - "**/db/src/**", - "**/env/src/**", - "**/logger/src/**", - "**/modules/auth/**", - "src/error/**", - "**/**.interface.ts", - "src/utils/randomId.ts", - "src/types/**", - "**/observability/src/**", - "**/packages/core/**", - "src/*.ts" -] \ No newline at end of file diff --git a/apps/backend/config/no-elysia-typebox.grit b/apps/backend/config/no-elysia-typebox.grit deleted file mode 100644 index 85b00c4..0000000 --- a/apps/backend/config/no-elysia-typebox.grit +++ /dev/null @@ -1,36 +0,0 @@ -engine biome(1.0) -language js(typescript) - -pattern from_module($name) { - JsModuleSource() as $m where { - $m <: `$name` - } -} - -pattern disallowed_identifier($name) { - JsIdentifierBinding() as $ib where { - $ib <: `$name`, - register_diagnostic( - span=$ib, - message="Avoid using Elysia TypeBox. Use Zod schema instead." - ) - } -} - -pattern disallowed_literal_export($name) { - JsLiteralExportName() as $en where { - $en <: $name, - register_diagnostic( - span=$en, - message="Avoid using Elysia TypeBox. Use Zod schema instead." - ) - } -} - -JsImport() as $i where { - $i <: or { - contains disallowed_identifier(name = `t`), - contains disallowed_literal_export(name = "t") - }, - $i <: contains from_module(name = `"elysia"`) -} diff --git a/apps/backend/config/no-try-in-handler.grit b/apps/backend/config/no-try-in-handler.grit deleted file mode 100644 index a313a62..0000000 --- a/apps/backend/config/no-try-in-handler.grit +++ /dev/null @@ -1,33 +0,0 @@ -engine biome(1.0) -language js(typescript) - -pattern new_expr($identifier) { - JsNewExpression() as $n where { - $n <: contains JsIdentifierExpression() as $ie where { - $ie <: contains JsReferenceIdentifier() as $i where { - $i <: `$identifier` - } - } - } -} - -JsCallExpression() as $c where { - $c <: contains JsStaticMemberExpression() as $sme where { - $sme <: contains JsName() as $jsn where { - $jsn <: or { `get`, `post`, `put`, `patch`, `delete`, `all` } - } - }, - $c <: contains new_expr(identifier = `Elysia`), - $c <: contains JsCallArgumentList[$arg1, $arg2, ...] where { - $arg1 <: contains JsStringLiteralExpression(), - $arg2 <: contains JsArrowFunctionExpression() as $func where { - $func <: contains JsTryStatement() as $try where { - register_diagnostic( - span=$try, - message="Avoid using try-catch statement in route handlers. Errors should be handled by separate ErrorHandler.", - severity="warn" - ) - } - } - } -} diff --git a/apps/backend/config/no-zod-any.grit b/apps/backend/config/no-zod-any.grit deleted file mode 100644 index b2774fa..0000000 --- a/apps/backend/config/no-zod-any.grit +++ /dev/null @@ -1,9 +0,0 @@ -engine biome(1.0) -language js(typescript) - -`z.any()` as $any where { - register_diagnostic( - span = $any, - message = "Unexpected z.any(). Specify a different schema." - ) -} diff --git a/apps/backend/package.json b/apps/backend/package.json index e8d32f9..53de1ae 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "name": "@project-cvsa/backend", "type": "module", "version": "0.0.1", @@ -34,47 +33,4 @@ "@project-cvsa/db": "workspace:*", "@project-cvsa/configs": "workspace:*" } -======= - "name": "@cvsa/backend", - "type": "module", - "version": "2.1.0", - "scripts": { - "lint": "bunx --bun biome lint", - "dev": "NODE_ENV=development bun --watch src/index.ts", - "start": "bun src/index.ts", - "test": "bun test --concurrent", - "test:coverage": "bun test --coverage --concurrent", - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.4", - "@types/bun": "latest", - "@cvsa/typescript-config": "workspace:*" - }, - "dependencies": { - "@better-auth/prisma-adapter": "^1.5.4", - "@cvsa/core": "workspace:*", - "@cvsa/db": "workspace:*", - "@cvsa/env": "workspace:*", - "@cvsa/logger": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "@elysiajs/openapi": "^1.4.14", - "@elysiajs/opentelemetry": "^1.4.10", - "@fanee/runtime-node": "^0.2.1", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", - "@opentelemetry/sdk-trace-node": "^2.6.1", - "@prisma/adapter-pg": "^7.4.1", - "@t3-oss/env-core": "^0.13.10", - "argon2": "^0.44.0", - "better-auth": "^1.5.4", - "chalk": "^5.6.2", - "elysia": "^1.4.26", - "elysia-ip": "^1.0.10", - "elysia-rate-limit": "^4.5.0", - "nanoid": "^5.1.6", - "pino": "^10.3.1", - "zod": "^4.3.6" - } ->>>>>>> origin/develop } diff --git a/apps/backend/src/common/error/RateLimitError.ts b/apps/backend/src/common/error/RateLimitError.ts deleted file mode 100644 index c192204..0000000 --- a/apps/backend/src/common/error/RateLimitError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AppError } from "@cvsa/core"; - -export class RateLimitError extends AppError { - constructor(message = "Request rate limit exceeded") { - super(message, "RATE_LIMIT_ERROR", 429); - } -} diff --git a/apps/backend/src/common/error/getErrorResponse.ts b/apps/backend/src/common/error/getErrorResponse.ts deleted file mode 100644 index 677127e..0000000 --- a/apps/backend/src/common/error/getErrorResponse.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ErrorResponseDto } from "@cvsa/core"; -import { i18nRuntime } from "@/common/i18n"; -import { getTraceId } from "@/common/trace"; - -// biome-ignore lint/suspicious/noExplicitAny: Utility type -type StatusFunc = (code: number, response: any) => any; - -export const getErrorResponse = ( - statusFunc: T, - statusCode: number, - locale: string | undefined, - data: ErrorResponseDto -): ReturnType => { - const traceId = getTraceId(); - const { message } = data; - const translatedMessages = message ? i18nRuntime.tAll(message) : undefined; - const translatedMessage = message ? i18nRuntime.t({ locale })(message) : undefined; - return statusFunc(statusCode, { - ...data, - message: translatedMessage, - i18n: locale === undefined ? translatedMessages : undefined, - traceId, - }); -}; diff --git a/apps/backend/src/common/error/index.ts b/apps/backend/src/common/error/index.ts deleted file mode 100644 index 14ae090..0000000 --- a/apps/backend/src/common/error/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./RateLimitError"; -export * from "./getErrorResponse"; diff --git a/apps/backend/src/common/i18n.ts b/apps/backend/src/common/i18n.ts deleted file mode 100644 index c82184d..0000000 --- a/apps/backend/src/common/i18n.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { env } from "@cvsa/env"; -import { FaneeRuntime } from "@fanee/runtime-node"; -import path from "node:path"; - -const getBundleLocation = () => { - if (env.NODE_ENV !== "production") { - return path.join(import.meta.dir, "../../../../locale/"); - } else { - return path.join(import.meta.dir, "./locale/"); - } -}; - -export const i18nRuntime: FaneeRuntime = new FaneeRuntime({ - bundlePath: getBundleLocation(), - defaultLocale: "zh", - namespace: "backend", -}); - -await i18nRuntime.load(); diff --git a/apps/backend/src/common/log.ts b/apps/backend/src/common/log.ts deleted file mode 100644 index d9582bc..0000000 --- a/apps/backend/src/common/log.ts +++ /dev/null @@ -1,50 +0,0 @@ -import chalk from "chalk"; - -export function getLogLevelForRequest(status: number): "error" | "warn" | "info" { - if (status >= 500) return "error"; - if (status >= 400) return "warn"; - return "info"; -} - -interface GinLogData { - method?: string; - path?: string; - status?: number; - latency?: string; - ip?: string; -} - -export function formatGinLog(data: GinLogData = {}): string { - const parts: string[] = []; - - if (data.method) { - const m = data.method.toUpperCase(); - let methodBg = chalk.bgBlue.white.bold; - if (m === "POST") methodBg = chalk.bgCyan.black.bold; - else if (m === "PUT") methodBg = chalk.bgYellow.black.bold; - else if (m === "DELETE") methodBg = chalk.bgRed.white.bold; - else if (m === "PATCH") methodBg = chalk.bgGreen.white.bold; - parts.push(methodBg(` ${m.padEnd(6)} `)); - } - - if (data.status) { - let statusColor = chalk.green; - if (data.status >= 400 && data.status < 500) statusColor = chalk.yellow; - else if (data.status >= 500) statusColor = chalk.red; - parts.push(statusColor(data.status.toString().padStart(3))); - } - - if (data.latency !== undefined) { - parts.push(chalk.magenta(data.latency.padStart(10))); - } - - if (data.ip) { - parts.push(chalk.gray(data.ip.padStart(15))); - } - - if (data.path) { - parts.push(`"${data.path}"`); - } - - return parts.join(" | "); -} diff --git a/apps/backend/src/common/trace.ts b/apps/backend/src/common/trace.ts deleted file mode 100644 index 9be36ee..0000000 --- a/apps/backend/src/common/trace.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SpanStatusCode, trace, context } from "@opentelemetry/api"; -import pkg from "../../package.json"; - -export const tracer = trace.getTracer(pkg.name, pkg.version); - -export async function traceTask(name: string, fn: () => T): Promise { - return tracer.startActiveSpan(name, async (span) => { - try { - span.setStatus({ code: SpanStatusCode.OK }); - return await fn(); - } catch (e) { - span.setStatus({ code: SpanStatusCode.ERROR }); - throw e; - } finally { - span.end(); - } - }); -} - -export const getTraceId = () => { - const currentSpan = trace.getSpan(context.active()); - - if (currentSpan) { - const spanContext = currentSpan.spanContext(); - - return spanContext.traceId; - } -}; diff --git a/apps/backend/src/errorHandler.ts b/apps/backend/src/errorHandler.ts deleted file mode 100644 index 69728ef..0000000 --- a/apps/backend/src/errorHandler.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Elysia, ElysiaCustomStatusResponse } from "elysia"; -import { AppError, BetterAuthAPIError } from "@cvsa/core"; -import { appLogger, pinoLogger } from "@cvsa/logger"; -import type { ErrorResponseDto } from "@cvsa/core"; -import { getErrorResponse } from "@/common/error"; -import { getTraceId } from "@/common/trace"; -import { i18nMiddleware } from "@/middlewares"; -import { ip } from "elysia-ip"; -import { formatGinLog, getLogLevelForRequest } from "@common/log"; - -const AUTH_INVALID_CODES = [ - "INVALID_CREDENTIALS", - "INVALID_EMAIL_OR_PASSWORD", - "USER_NOT_FOUND", - "EMAIL_NOT_VERIFIED", -] as const; - -type AuthInvalidCode = (typeof AUTH_INVALID_CODES)[number]; - -interface ErrorStoreData { - status: number; - request: Request; - data: ErrorResponseDto; - locale?: string; - rawError?: unknown; - known: boolean; -} - -type ErrorStore = Record; - -export const errorHandler = new Elysia() - .error({ - AppError, - }) - .state("error", {} as ErrorStore) - .state("startTime", {} as Record) - .use(ip()) - .onRequest(function setRequestMeta({ store }) { - store.startTime = {}; - store.error = {}; - const traceId = getTraceId() ?? ""; - if (traceId) { - store.startTime[traceId] = Bun.nanoseconds() / 1_000_000; - } - }) - .use(i18nMiddleware) - .onError({ as: "global" }, ({ code, error, set, store, request }) => { - const traceId = getTraceId() ?? ""; - if (traceId) { - set.headers["X-Trace-ID"] = traceId; - } - - const setErrorResponse = (status: number, data: ErrorResponseDto) => { - store.error[traceId] = { - data, - status, - locale: store.locale[traceId], - request, - rawError: error, - known: true, - }; - }; - - if (AppError.isAppError(error)) { - setErrorResponse(error.statusCode, { - code: error.code as ErrorResponseDto["code"], - message: error.message, - }); - } else if (code === "NOT_FOUND") { - setErrorResponse(404, { - code: "NOT_FOUND", - message: "error.not-found", - }); - } else if (code === "VALIDATION") { - // const detail = error.detail(error.message); - // const message = typeof detail === "string" ? detail : detail.summary; - setErrorResponse(422, { - code: "VALIDATION_ERROR", - message: "error.validation", - }); - } else if (error instanceof BetterAuthAPIError) { - const bodyCode = error.body?.code || ""; - - if (bodyCode === "USERNAME_IS_ALREADY_TAKEN") { - setErrorResponse(409, { - code: bodyCode, - message: "error.username-taken", - }); - } else if (bodyCode === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") { - setErrorResponse(409, { - code: bodyCode, - message: "error.email-taken", - }); - } else if (AUTH_INVALID_CODES.includes(bodyCode as AuthInvalidCode)) { - setErrorResponse(401, { - code: "INVALID_CREDENTIALS", - message: "error.invalid-cred", - }); - } else if (error.statusCode < 500) { - setErrorResponse(error.statusCode, { - code: "UNAUTHORIZED", - message: error.body?.message, - }); - } - } else if (error instanceof ElysiaCustomStatusResponse) { - if (error.code === 404) { - setErrorResponse(error.code, { - code: "NOT_FOUND", - message: "error.not-found", - }); - } - } else { - setErrorResponse(500, { - code: "INTERNAL_SERVER_ERROR", - message: "error.internal", - }); - store.error[traceId].known = false; - } - }) - .onError({ as: "global" }, function logErrorResponse({ store, status: statusFunc, ip }) { - const traceId = getTraceId() ?? ""; - const { request, locale, data, status, rawError, known } = store.error[traceId]; - const startTime = store.startTime[traceId]; - const requestPath = new URL(request.url).pathname; - - const userAgent = request.headers?.get("user-agent") ?? undefined; - - const logData = { - type: "http_request", - method: request.method, - path: requestPath, - status, - latency: startTime - ? `${(Bun.nanoseconds() / 1_000_000 - startTime).toFixed(3)}ms` - : undefined, - userAgent, - ip, - error: Bun.inspect(rawError), - errorCode: data.code, - }; - if (!known) { - appLogger[getLogLevelForRequest(status ?? 500)](logData.error, data); - } - pinoLogger[getLogLevelForRequest(status ?? 500)](formatGinLog(logData)); - return getErrorResponse(statusFunc, status, locale, data); - }); diff --git a/apps/backend/src/handlers/auth/currentUser.ts b/apps/backend/src/handlers/auth/currentUser.ts deleted file mode 100644 index 6768410..0000000 --- a/apps/backend/src/handlers/auth/currentUser.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Elysia } from "elysia"; -import { - CurrentUserInfoSchema, - betterAuthToCurrentUserInfoDto, - ErrorResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const getCurrentUserHandler = new Elysia().use(authMiddleware).get( - "/me", - async ({ status, session }) => { - const userInfo = await traceTask("getCurrentUser", async () => { - return betterAuthToCurrentUserInfoDto(session.user); - }); - return status(200, userInfo); - }, - { - detail: { - summary: "Get current user", - description: - "Retrieve the currently authenticated user's profile information. Requires a valid session cookie or a valid token in Authorization header.", - }, - response: { - 200: CurrentUserInfoSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 422: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); diff --git a/apps/backend/src/handlers/auth/index.ts b/apps/backend/src/handlers/auth/index.ts deleted file mode 100644 index 7673ad3..0000000 --- a/apps/backend/src/handlers/auth/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Elysia from "elysia"; -import { signupHandler } from "./signup"; -import { getCurrentUserHandler } from "./currentUser"; -import { loginHandler } from "./login"; -import { logoutHandler } from "./logout"; - -export const authHandler = new Elysia() - .use(signupHandler) - .use(getCurrentUserHandler) - .use(loginHandler) - .use(logoutHandler); diff --git a/apps/backend/src/handlers/auth/login.ts b/apps/backend/src/handlers/auth/login.ts deleted file mode 100644 index 4d7b7f1..0000000 --- a/apps/backend/src/handlers/auth/login.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Elysia } from "elysia"; -import { ip } from "elysia-ip"; -import { - AppError, - auth, - LoginRequestSchema, - LoginResponseSchema, - ErrorResponseSchema, - betterAuthToLoginUserInfoDto, - toLoginResponse, - toBetterAuthHeaders, -} from "@cvsa/core"; -import { traceTask } from "@/common/trace"; - -const DAY = 86400; - -export const loginHandler = new Elysia().use(ip()).post( - "/session", - async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await traceTask("auth.signInEmail", async () => { - return await auth.api.signInEmail({ - body: { - email: body.email, - password: body.password, - }, - headers: toBetterAuthHeaders(headers), - }); - }); - - if (!token) { - throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, { - cause: "Better Auth responded with no token", - }); - } - - tokenCookie.value = token; - tokenCookie.httpOnly = true; - tokenCookie.maxAge = 90 * DAY; - tokenCookie.secure = true; - tokenCookie.sameSite = "lax"; - - const userInfo = betterAuthToLoginUserInfoDto(user, token); - const response = toLoginResponse(userInfo); - - return status(200, response); - }, - { - body: LoginRequestSchema, - detail: { - summary: "User Login", - description: - "Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.", - }, - response: { - 200: LoginResponseSchema, - 401: ErrorResponseSchema, - 422: ErrorResponseSchema, - 429: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); diff --git a/apps/backend/src/handlers/auth/logout.ts b/apps/backend/src/handlers/auth/logout.ts deleted file mode 100644 index 54bff43..0000000 --- a/apps/backend/src/handlers/auth/logout.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { authMiddleware } from "@/middlewares"; -import { auth, ErrorResponseSchema, toBetterAuthHeaders } from "@cvsa/core"; -import { traceTask } from "@/common/trace"; - -export const logoutHandler = new Elysia().use(authMiddleware).delete( - "/session", - async ({ set, headers, cookie: { token: tokenCookie } }) => { - await traceTask("auth.signOut", async () => { - return await auth.api.signOut({ - headers: toBetterAuthHeaders(headers), - }); - }); - - tokenCookie.remove(); - - set.status = 204; - return null; - }, - { - detail: { - summary: "User Logout", - description: - "Terminate the current user session. Removes the authentication cookie and invalidates the session server-side.", - }, - response: { - 204: z.null(), - 401: ErrorResponseSchema, - }, - cookie: z.object({ - token: z.optional(z.string()), - }), - } -); diff --git a/apps/backend/src/handlers/auth/signup.ts b/apps/backend/src/handlers/auth/signup.ts deleted file mode 100644 index 8a0a77d..0000000 --- a/apps/backend/src/handlers/auth/signup.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Elysia } from "elysia"; -import { ip } from "elysia-ip"; -import { - betterAuthToSignupUserInfoDto, - SignupRequestSchema, - signupRequestToBetterAuth, - SignupResponseSchema, - toSignUpResponse, - ErrorResponseSchema, - toBetterAuthHeaders, -} from "@cvsa/core"; -import { AppError } from "@cvsa/core"; -import { auth } from "@cvsa/core"; -import { traceTask } from "@/common/trace"; - -const DAY = 86400; - -export const signupHandler = new Elysia().use(ip()).post( - "/user", - async ({ body, status, headers, cookie: { token: tokenCookie } }) => { - const { user, token } = await traceTask("auth.signUpEmail", async () => { - return await auth.api.signUpEmail({ - body: signupRequestToBetterAuth(body), - headers: toBetterAuthHeaders(headers), - }); - }); - - if (!token) { - throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, { - cause: "Better Auth responded with no token", - }); - } - - tokenCookie.value = token; - tokenCookie.httpOnly = true; - tokenCookie.maxAge = 90 * DAY; - tokenCookie.secure = true; - tokenCookie.sameSite = "lax"; - - const userInfo = betterAuthToSignupUserInfoDto(user, token); - const response = toSignUpResponse(userInfo); - - return status(200, response); - }, - { - body: SignupRequestSchema, - detail: { - summary: "User Registration", - description: - "Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.", - }, - response: { - 200: SignupResponseSchema, - 400: ErrorResponseSchema, - 422: ErrorResponseSchema, - 429: ErrorResponseSchema, - 500: ErrorResponseSchema, - }, - } -); diff --git a/apps/backend/src/handlers/catalog/artist/create.ts b/apps/backend/src/handlers/catalog/artist/create.ts deleted file mode 100644 index d4de3d0..0000000 --- a/apps/backend/src/handlers/catalog/artist/create.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Elysia } from "elysia"; -import { - CreateArtistRequestSchema, - ErrorResponseSchema, - ArtistResponseSchema, - artistService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistCreateHandler = new Elysia({ name: "artistCreateHandler" }) - .use(authMiddleware) - .post( - "/artist", - async ({ body, status }) => { - const artist = await traceTask("artistService.create", async () => { - return await artistService.create(body); - }); - return status(201, artist); - }, - { - body: CreateArtistRequestSchema, - detail: { - summary: "Create Artist", - description: "Create a new artist entry in the catalog. Requires authentication.", - }, - response: { - 201: ArtistResponseSchema, - 401: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/artist/delete.ts b/apps/backend/src/handlers/catalog/artist/delete.ts deleted file mode 100644 index d6abec0..0000000 --- a/apps/backend/src/handlers/catalog/artist/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { ErrorResponseSchema, artistService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistDeleteHandler = new Elysia({ name: "artistDeleteHandler" }) - .use(authMiddleware) - .delete( - "/artist/:id", - async ({ params, set }) => { - await traceTask("artistService.delete", async () => { - return await artistService.delete(params.id); - }); - set.status = 204; - return null; - }, - { - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Delete Artist", - description: - "Soft delete an artist from the catalog. Requires authentication. The artist record is marked as deleted but retained in the database for data integrity.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/artist/get.ts b/apps/backend/src/handlers/catalog/artist/get.ts deleted file mode 100644 index 9e8096f..0000000 --- a/apps/backend/src/handlers/catalog/artist/get.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, ArtistDetailsResponseSchema, artistService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const artistDetailsHandler = new Elysia().get( - "/artist/:id", - async ({ params, status }) => { - const artist = await traceTask("artistService.getDetails", async () => { - return await artistService.getDetails(params.id); - }); - return status(200, artist); - }, - { - detail: { - summary: "Artist Details", - description: - "Retrieve detailed information about a specific artist by its ID. Includes name, description, aliases, and metadata.", - }, - response: { - 200: ArtistDetailsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/artist/index.ts b/apps/backend/src/handlers/catalog/artist/index.ts deleted file mode 100644 index 4adfb99..0000000 --- a/apps/backend/src/handlers/catalog/artist/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Elysia } from "elysia"; -import { artistCreateHandler } from "./create"; -import { artistUpdateHandler } from "./update"; -import { artistDeleteHandler } from "./delete"; -import { artistDetailsHandler } from "./get"; -import { artistSearchHandler } from "./search"; - -export const artistHandler = new Elysia({ name: "artistHandler" }) - .use(artistDetailsHandler) - .use(artistCreateHandler) - .use(artistUpdateHandler) - .use(artistDeleteHandler) - .use(artistSearchHandler); diff --git a/apps/backend/src/handlers/catalog/artist/search.ts b/apps/backend/src/handlers/catalog/artist/search.ts deleted file mode 100644 index fd05288..0000000 --- a/apps/backend/src/handlers/catalog/artist/search.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Elysia } from "elysia"; -import { artistSearchService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -// TODO: add corresponding DTO and response schema -export const artistSearchHandler = new Elysia().get( - "/artists", - async ({ query, status }) => { - const song = await traceTask("artistSearchService.search", async () => { - return await artistSearchService.search(query.q || ""); - }); - return status(200, song); - }, - { - detail: { - summary: "Search for Artists", - description: - "Full-text search across artist names, aliases, and descriptions. Returns a list of matching artists ordered by relevance.", - }, - query: z.object({ - q: z.string().optional(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/artist/update.ts b/apps/backend/src/handlers/catalog/artist/update.ts deleted file mode 100644 index 6162564..0000000 --- a/apps/backend/src/handlers/catalog/artist/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { - UpdateArtistRequestSchema, - ErrorResponseSchema, - artistService, - ArtistResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistUpdateHandler = new Elysia({ name: "artistUpdateHandler" }) - .use(authMiddleware) - .patch( - "/artist/:id", - async ({ params, body, status }) => { - const artist = await traceTask("artistService.update", async () => { - return await artistService.update(params.id, body); - }); - return status(200, artist); - }, - { - body: UpdateArtistRequestSchema, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Update Artist", - description: - "Update metadata of an existing artist identified by its ID. Requires authentication.", - }, - response: { - 200: ArtistResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/artistRole/create.ts b/apps/backend/src/handlers/catalog/artistRole/create.ts deleted file mode 100644 index 30eaf21..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/create.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Elysia } from "elysia"; -import { - CreateArtistRoleRequestSchema, - ErrorResponseSchema, - ArtistRoleResponseSchema, - artistRoleService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistRoleCreateHandler = new Elysia({ name: "artistRoleCreateHandler" }) - .use(authMiddleware) - .post( - "/artist-role", - async ({ body, status }) => { - const role = await traceTask("artistRoleService.create", async () => { - return await artistRoleService.create(body); - }); - return status(201, role); - }, - { - body: CreateArtistRoleRequestSchema, - detail: { - summary: "Create Artist Role", - description: - "Create a new artist role entry in the catalog. Requires authentication.", - }, - response: { - 201: ArtistRoleResponseSchema, - 401: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/artistRole/delete.ts b/apps/backend/src/handlers/catalog/artistRole/delete.ts deleted file mode 100644 index 498ede0..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { ErrorResponseSchema, artistRoleService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistRoleDeleteHandler = new Elysia({ name: "artistRoleDeleteHandler" }) - .use(authMiddleware) - .delete( - "/artist-role/:id", - async ({ params, set }) => { - await traceTask("artistRoleService.delete", async () => { - return await artistRoleService.delete(params.id); - }); - set.status = 204; - return null; - }, - { - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Delete Artist Role", - description: - "Soft delete an artist role from the catalog. Requires authentication. The role record is marked as deleted but retained in the database for data integrity.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/artistRole/get.ts b/apps/backend/src/handlers/catalog/artistRole/get.ts deleted file mode 100644 index 7857ce2..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/get.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Elysia } from "elysia"; -import { - ErrorResponseSchema, - ArtistRoleDetailsResponseSchema, - artistRoleService, -} from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const artistRoleDetailsHandler = new Elysia().get( - "/artist-role/:id", - async ({ params, status }) => { - const role = await traceTask("artistRoleService.getDetails", async () => { - return await artistRoleService.getDetails(params.id); - }); - return status(200, role); - }, - { - detail: { - summary: "Artist Role Details", - description: "Retrieve detailed information about a specific artist role by its ID.", - }, - response: { - 200: ArtistRoleDetailsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/artistRole/index.ts b/apps/backend/src/handlers/catalog/artistRole/index.ts deleted file mode 100644 index 1d01520..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Elysia } from "elysia"; -import { artistRoleCreateHandler } from "./create"; -import { artistRoleUpdateHandler } from "./update"; -import { artistRoleDeleteHandler } from "./delete"; -import { artistRoleDetailsHandler } from "./get"; -import { artistRoleSearchHandler } from "./search"; - -export const artistRoleHandler = new Elysia({ name: "artistRoleHandler" }) - .use(artistRoleDetailsHandler) - .use(artistRoleCreateHandler) - .use(artistRoleUpdateHandler) - .use(artistRoleDeleteHandler) - .use(artistRoleSearchHandler); diff --git a/apps/backend/src/handlers/catalog/artistRole/search.ts b/apps/backend/src/handlers/catalog/artistRole/search.ts deleted file mode 100644 index 6542fcb..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/search.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Elysia } from "elysia"; -import { artistRoleSearchService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -// TODO: add corresponding DTO and response schema -export const artistRoleSearchHandler = new Elysia().get( - "/artist-roles", - async ({ query, status }) => { - const result = await traceTask("artistRoleSearchService.search", async () => { - return await artistRoleSearchService.search(query.q || ""); - }); - return status(200, result); - }, - { - detail: { - summary: "Search for Artist Roles", - description: - "Full-text search across artist role names. Returns a list of matching roles ordered by relevance.", - }, - query: z.object({ - q: z.string().optional(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/artistRole/update.ts b/apps/backend/src/handlers/catalog/artistRole/update.ts deleted file mode 100644 index 759418f..0000000 --- a/apps/backend/src/handlers/catalog/artistRole/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { - UpdateArtistRoleRequestSchema, - ErrorResponseSchema, - artistRoleService, - ArtistRoleResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const artistRoleUpdateHandler = new Elysia({ name: "artistRoleUpdateHandler" }) - .use(authMiddleware) - .patch( - "/artist-role/:id", - async ({ params, body, status }) => { - const role = await traceTask("artistRoleService.update", async () => { - return await artistRoleService.update(params.id, body); - }); - return status(200, role); - }, - { - body: UpdateArtistRoleRequestSchema, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Update Artist Role", - description: - "Update metadata of an existing artist role identified by its ID. Requires authentication.", - }, - response: { - 200: ArtistRoleResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/engine/create.ts b/apps/backend/src/handlers/catalog/engine/create.ts deleted file mode 100644 index 95243e6..0000000 --- a/apps/backend/src/handlers/catalog/engine/create.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Elysia } from "elysia"; -import { - CreateEngineRequestSchema, - ErrorResponseSchema, - EngineResponseSchema, - engineService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const engineCreateHandler = new Elysia({ name: "engineCreateHandler" }) - .use(authMiddleware) - .post( - "/engine", - async ({ body, status }) => { - const engine = await traceTask("engineService.create", async () => { - return await engineService.create(body); - }); - return status(201, engine); - }, - { - body: CreateEngineRequestSchema, - detail: { - summary: "Create Engine", - description: - "Create a new SVS engine entry in the catalog. Requires authentication.", - }, - response: { - 201: EngineResponseSchema, - 401: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/engine/delete.ts b/apps/backend/src/handlers/catalog/engine/delete.ts deleted file mode 100644 index b483c5a..0000000 --- a/apps/backend/src/handlers/catalog/engine/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { ErrorResponseSchema, engineService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const engineDeleteHandler = new Elysia({ name: "engineDeleteHandler" }) - .use(authMiddleware) - .delete( - "/engine/:id", - async ({ params, set }) => { - await traceTask("engineService.delete", async () => { - return await engineService.delete(params.id); - }); - set.status = 204; - return null; - }, - { - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Delete Engine", - description: - "Soft delete an SVS engine from the catalog. Requires authentication. The engine record is marked as deleted but retained in the database for data integrity.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/engine/details.ts b/apps/backend/src/handlers/catalog/engine/details.ts deleted file mode 100644 index 3233e5c..0000000 --- a/apps/backend/src/handlers/catalog/engine/details.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, EngineDetailsResponseSchema, engineService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const engineDetailsHandler = new Elysia().get( - "/engine/:id/details", - async ({ params, status }) => { - const engine = await traceTask("engineService.getDetails", async () => { - return await engineService.getDetails(params.id); - }); - return status(200, engine); - }, - { - detail: { - summary: "Engine Details", - description: - "Retrieve detailed information about a specific SVS engine by its ID. Includes name, description, and metadata.", - }, - response: { - 200: EngineDetailsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/engine/index.ts b/apps/backend/src/handlers/catalog/engine/index.ts deleted file mode 100644 index fee3441..0000000 --- a/apps/backend/src/handlers/catalog/engine/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Elysia } from "elysia"; -import { engineCreateHandler } from "./create"; -import { engineUpdateHandler } from "./update"; -import { engineDeleteHandler } from "./delete"; -import { engineDetailsHandler } from "./details"; - -export const engineHandler = new Elysia({ name: "engineHandler" }) - .use(engineDetailsHandler) - .use(engineCreateHandler) - .use(engineUpdateHandler) - .use(engineDeleteHandler); diff --git a/apps/backend/src/handlers/catalog/engine/update.ts b/apps/backend/src/handlers/catalog/engine/update.ts deleted file mode 100644 index 90c1193..0000000 --- a/apps/backend/src/handlers/catalog/engine/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { - UpdateEngineRequestSchema, - ErrorResponseSchema, - engineService, - EngineResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const engineUpdateHandler = new Elysia({ name: "engineUpdateHandler" }) - .use(authMiddleware) - .patch( - "/engine/:id", - async ({ params, body, status }) => { - const engine = await traceTask("engineService.update", async () => { - return await engineService.update(params.id, body); - }); - return status(200, engine); - }, - { - body: UpdateEngineRequestSchema, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Update Engine", - description: - "Update metadata of an existing SVS engine identified by its ID. Requires authentication.", - }, - response: { - 200: EngineResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/singer/create.ts b/apps/backend/src/handlers/catalog/singer/create.ts deleted file mode 100644 index 42c6195..0000000 --- a/apps/backend/src/handlers/catalog/singer/create.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Elysia } from "elysia"; -import { - CreateSingerRequestSchema, - ErrorResponseSchema, - SingerResponseSchema, - singerService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const singerCreateHandler = new Elysia({ name: "singerCreateHandler" }) - .use(authMiddleware) - .post( - "/singer", - async ({ body, status }) => { - const singer = await traceTask("singerService.create", async () => { - return await singerService.create(body); - }); - return status(201, singer); - }, - { - body: CreateSingerRequestSchema, - detail: { - summary: "Create Singer", - description: "Create a new singer entry in the catalog. Requires authentication.", - }, - response: { - 201: SingerResponseSchema, - 401: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/singer/delete.ts b/apps/backend/src/handlers/catalog/singer/delete.ts deleted file mode 100644 index 4611170..0000000 --- a/apps/backend/src/handlers/catalog/singer/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { ErrorResponseSchema, singerService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const singerDeleteHandler = new Elysia({ name: "singerDeleteHandler" }) - .use(authMiddleware) - .delete( - "/singer/:id", - async ({ params, set }) => { - await traceTask("singerService.delete", async () => { - return await singerService.delete(params.id); - }); - set.status = 204; - return null; - }, - { - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Delete Singer", - description: - "Soft delete a singer from the catalog. Requires authentication. The singer record is marked as deleted but retained in the database for data integrity.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/singer/get.ts b/apps/backend/src/handlers/catalog/singer/get.ts deleted file mode 100644 index d9810d2..0000000 --- a/apps/backend/src/handlers/catalog/singer/get.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, SingerDetailsResponseSchema, singerService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const singerGetHandler = new Elysia().get( - "/singer/:id", - async ({ params, status }) => { - const singer = await traceTask("singerService.getDetails", async () => { - return await singerService.getDetails(params.id); - }); - return status(200, singer); - }, - { - detail: { - summary: "Get Singer", - description: - "Retrieve detailed information about a specific singer by its ID. Includes name, description, language, and metadata.", - }, - response: { - 200: SingerDetailsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/singer/index.ts b/apps/backend/src/handlers/catalog/singer/index.ts deleted file mode 100644 index afa6e42..0000000 --- a/apps/backend/src/handlers/catalog/singer/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Elysia } from "elysia"; -import { singerCreateHandler } from "./create"; -import { singerUpdateHandler } from "./update"; -import { singerDeleteHandler } from "./delete"; -import { singerGetHandler } from "./get"; -import { singerSearchHandler } from "./search"; - -export const singerHandler = new Elysia({ name: "singerHandler" }) - .use(singerGetHandler) - .use(singerCreateHandler) - .use(singerUpdateHandler) - .use(singerDeleteHandler) - .use(singerSearchHandler); diff --git a/apps/backend/src/handlers/catalog/singer/search.ts b/apps/backend/src/handlers/catalog/singer/search.ts deleted file mode 100644 index 2485da9..0000000 --- a/apps/backend/src/handlers/catalog/singer/search.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Elysia } from "elysia"; -import { singerSearchService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -// TODO: add corresponding DTO and response schema -export const singerSearchHandler = new Elysia().get( - "/singers", - async ({ query, status }) => { - const song = await traceTask("singerSearchService.search", async () => { - return await singerSearchService.search(query.q || ""); - }); - return status(200, song); - }, - { - detail: { - summary: "Search for Singers", - description: - "Full-text search across singer names, and descriptions. Returns a list of matching signers ordered by relevance.", - }, - query: z.object({ - q: z.string().optional(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/singer/update.ts b/apps/backend/src/handlers/catalog/singer/update.ts deleted file mode 100644 index 519e93b..0000000 --- a/apps/backend/src/handlers/catalog/singer/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { - UpdateSingerRequestSchema, - ErrorResponseSchema, - singerService, - SingerResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const singerUpdateHandler = new Elysia({ name: "singerUpdateHandler" }) - .use(authMiddleware) - .patch( - "/singer/:id", - async ({ params, body, status }) => { - const singer = await traceTask("singerService.update", async () => { - return await singerService.update(params.id, body); - }); - return status(200, singer); - }, - { - body: UpdateSingerRequestSchema, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Update Singer", - description: - "Update metadata of an existing singer identified by its ID. Requires authentication.", - }, - response: { - 200: SingerResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/song/create.ts b/apps/backend/src/handlers/catalog/song/create.ts deleted file mode 100644 index b5fc7e1..0000000 --- a/apps/backend/src/handlers/catalog/song/create.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Elysia } from "elysia"; -import { - CreateSongRequestSchema, - ErrorResponseSchema, - SongResponseSchema, - songService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const songCreateHandler = new Elysia({ name: "songCreateHandler" }).use(authMiddleware).post( - "/song", - async ({ body, status }) => { - const song = await traceTask("songService.create", async () => { - return await songService.create(body); - }); - return status(201, song); - }, - { - body: CreateSongRequestSchema, - detail: { - summary: "Create Song", - description: "Create a new song entry in the catalog. Requires authentication.", - }, - response: { - 201: SongResponseSchema, - 401: ErrorResponseSchema, - }, - } -); diff --git a/apps/backend/src/handlers/catalog/song/delete.ts b/apps/backend/src/handlers/catalog/song/delete.ts deleted file mode 100644 index fe86ddc..0000000 --- a/apps/backend/src/handlers/catalog/song/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { ErrorResponseSchema, songService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const songDeleteHandler = new Elysia({ name: "songDeleteHandler" }) - .use(authMiddleware) - .delete( - "/song/:id", - async ({ params, set }) => { - await traceTask("songService.delete", async () => { - return await songService.delete(params.id); - }); - set.status = 204; - return null; - }, - { - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Delete Song", - description: - "Soft delete a song from the catalog. Requires authentication. The song record is marked as deleted but retained in the database for data integrity.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/catalog/song/details.ts b/apps/backend/src/handlers/catalog/song/details.ts deleted file mode 100644 index 7fe95f2..0000000 --- a/apps/backend/src/handlers/catalog/song/details.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, SongDetailsResponseSchema, songService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const songDetailsHandler = new Elysia().get( - "/song/:id/details", - async ({ params, status }) => { - const song = await traceTask("songService.getDetails", async () => { - return await songService.getDetails(params.id); - }); - return status(200, song); - }, - { - detail: { - summary: "Song Details", - description: - "Retrieve detailed information about a specific song by its ID. Includes all metadata such as title, artists, album, duration, and audio sources.", - }, - response: { - 200: SongDetailsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/song/index.ts b/apps/backend/src/handlers/catalog/song/index.ts deleted file mode 100644 index dc9e11c..0000000 --- a/apps/backend/src/handlers/catalog/song/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Elysia } from "elysia"; -import { songCreateHandler } from "./create"; -import { songUpdateHandler } from "./update"; -import { songDeleteHandler } from "./delete"; -import { songDetailsHandler } from "./details"; -import { songSearchHandler } from "./search"; -import { songLyricsHandler } from "./lyrics"; - -export const songHandler = new Elysia({ name: "songHandler" }) - .use(songDetailsHandler) - .use(songCreateHandler) - .use(songUpdateHandler) - .use(songDeleteHandler) - .use(songSearchHandler) - .use(songLyricsHandler); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/create.ts b/apps/backend/src/handlers/catalog/song/lyrics/create.ts deleted file mode 100644 index 33573ea..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/create.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Elysia } from "elysia"; -import { - ErrorResponseSchema, - SongLyricsCreateRequestSchema, - SongLyricsResponseSchema, - songService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; -import z from "zod"; - -export const songLyricsCreateHandler = new Elysia({ name: "songLyricsCreateHandler" }) - .use(authMiddleware) - .post( - "/song/:id/lyric", - async ({ body, params, status }) => { - const lyric = await traceTask("songService.createLyric", async () => { - return await songService.createLyric(params.id, body); - }); - return status(201, lyric); - }, - { - body: SongLyricsCreateRequestSchema, - detail: { - summary: "Create Song Lyric", - description: "Add a new lyrics item for a song. Requires authentication.", - }, - response: { - 201: SongLyricsResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } - ); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/delete.ts b/apps/backend/src/handlers/catalog/song/lyrics/delete.ts deleted file mode 100644 index 126e19f..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, songService } from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; -import z from "zod"; - -export const songLyricsDeleteHandler = new Elysia({ name: "songLyricsDeleteHandler" }) - .use(authMiddleware) - .delete( - "/song/:id/lyric/:lyricId", - async ({ params, set }) => { - await traceTask("songService.deleteLyric", async () => { - return await songService.deleteLyric(params.id, params.lyricId); - }); - set.status = 204; - return null; - }, - { - detail: { - summary: "Delete Song Lyric", - description: "Delete a specific lyrics item for a song. Requires authentication.", - }, - response: { - 204: z.null(), - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - lyricId: z.coerce.number().int().positive(), - }), - } - ); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/get.ts b/apps/backend/src/handlers/catalog/song/lyrics/get.ts deleted file mode 100644 index 97d6a39..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/get.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, SongLyricsResponseSchema, songService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const songLyricsGetHandler = new Elysia().get( - "/song/:id/lyric/:lyricId", - async ({ params, status }) => { - const lyric = await traceTask("songService.getLyric", async () => { - return await songService.getLyric(params.id, params.lyricId); - }); - return status(200, lyric); - }, - { - detail: { - summary: "Get Song Lyric", - description: "Retrieve a specific lyrics item for a song by its ID.", - }, - response: { - 200: SongLyricsResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - lyricId: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/index.ts b/apps/backend/src/handlers/catalog/song/lyrics/index.ts deleted file mode 100644 index 21b4a7e..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Elysia } from "elysia"; -import { songLyricsListHandler } from "./list"; -import { songLyricsGetHandler } from "./get"; -import { songLyricsCreateHandler } from "./create"; -import { songLyricsUpdateHandler } from "./update"; -import { songLyricsDeleteHandler } from "./delete"; - -export const songLyricsHandler = new Elysia({ name: "songLyricsHandler" }) - .use(songLyricsListHandler) - .use(songLyricsGetHandler) - .use(songLyricsCreateHandler) - .use(songLyricsUpdateHandler) - .use(songLyricsDeleteHandler); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/list.ts b/apps/backend/src/handlers/catalog/song/lyrics/list.ts deleted file mode 100644 index 3444506..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/list.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Elysia } from "elysia"; -import { ErrorResponseSchema, SongLyricsListResponseSchema, songService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -export const songLyricsListHandler = new Elysia().get( - "/song/:id/lyrics", - async ({ params, status }) => { - const lyrics = await traceTask("songService.listLyrics", async () => { - return await songService.listLyrics(params.id); - }); - return status(200, lyrics); - }, - { - detail: { - summary: "List Song Lyrics", - description: "Retrieve all available lyrics for a specific song by its ID.", - }, - response: { - 200: SongLyricsListResponseSchema, - 400: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/song/lyrics/update.ts b/apps/backend/src/handlers/catalog/song/lyrics/update.ts deleted file mode 100644 index 3d0f6ce..0000000 --- a/apps/backend/src/handlers/catalog/song/lyrics/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { - ErrorResponseSchema, - SongLyricsUpdateRequestSchema, - SongLyricsResponseSchema, - songService, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; -import z from "zod"; - -export const songLyricsUpdateHandler = new Elysia({ name: "songLyricsUpdateHandler" }) - .use(authMiddleware) - .patch( - "/song/:id/lyric/:lyricId", - async ({ body, params, status }) => { - const lyric = await traceTask("songService.updateLyric", async () => { - return await songService.updateLyric(params.id, params.lyricId, body); - }); - return status(200, lyric); - }, - { - body: SongLyricsUpdateRequestSchema, - detail: { - summary: "Update Song Lyric", - description: "Update a specific lyrics item for a song. Requires authentication.", - }, - response: { - 200: SongLyricsResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - params: z.object({ - id: z.coerce.number().int().positive(), - lyricId: z.coerce.number().int().positive(), - }), - } - ); diff --git a/apps/backend/src/handlers/catalog/song/search.ts b/apps/backend/src/handlers/catalog/song/search.ts deleted file mode 100644 index 729a059..0000000 --- a/apps/backend/src/handlers/catalog/song/search.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Elysia } from "elysia"; -import { songSearchService } from "@cvsa/core"; -import z from "zod"; -import { traceTask } from "@/common/trace"; - -// TODO: add corresponding DTO and response schema -export const songSearchHandler = new Elysia().get( - "/songs", - async ({ query, status }) => { - const song = await traceTask("songSearchService.search", async () => { - return await songSearchService.search(query.q || ""); - }); - return status(200, song); - }, - { - detail: { - summary: "Search for Songs", - description: - "Full-text search across song titles, artist names, and album titles. Returns a list of matching songs ordered by relevance.", - }, - query: z.object({ - q: z.string().optional(), - }), - } -); diff --git a/apps/backend/src/handlers/catalog/song/update.ts b/apps/backend/src/handlers/catalog/song/update.ts deleted file mode 100644 index 34c723b..0000000 --- a/apps/backend/src/handlers/catalog/song/update.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Elysia } from "elysia"; -import { z } from "zod"; -import { - UpdateSongRequestSchema, - ErrorResponseSchema, - songService, - SongResponseSchema, -} from "@cvsa/core"; -import { authMiddleware } from "@/middlewares"; -import { traceTask } from "@/common/trace"; - -export const songUpdateHandler = new Elysia({ name: "songUpdateHandler" }) - .use(authMiddleware) - .patch( - "/song/:id", - async ({ params, body, status }) => { - const song = await traceTask("songService.update", async () => { - return await songService.update(params.id, body); - }); - return status(200, song); - }, - { - body: UpdateSongRequestSchema, - params: z.object({ - id: z.coerce.number().int().positive(), - }), - detail: { - summary: "Update Song", - description: - "Update metadata of an existing song identified by its ID. Requires authentication.", - }, - response: { - 200: SongResponseSchema, - 400: ErrorResponseSchema, - 401: ErrorResponseSchema, - 404: ErrorResponseSchema, - }, - } - ); diff --git a/apps/backend/src/handlers/dev/gc.ts b/apps/backend/src/handlers/dev/gc.ts deleted file mode 100644 index 207b9d6..0000000 --- a/apps/backend/src/handlers/dev/gc.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Elysia } from "elysia"; -import { env } from "@cvsa/env"; - -export const gcHandler = new Elysia().get("/gc-result", async ({ status }) => { - if (env.NODE_ENV !== "development") { - throw status(404); - } - Bun.gc(); - return status(200, { - success: true, - }); -}); diff --git a/apps/backend/src/handlers/dev/index.ts b/apps/backend/src/handlers/dev/index.ts deleted file mode 100644 index 658936f..0000000 --- a/apps/backend/src/handlers/dev/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Elysia } from "elysia"; -import { getHeapSnapshotHandler } from "./snapshot"; -import { gcHandler } from "./gc"; - -export const devHandler = new Elysia({ name: "devHandler" }) - .use(getHeapSnapshotHandler) - .use(gcHandler); diff --git a/apps/backend/src/handlers/dev/snapshot.ts b/apps/backend/src/handlers/dev/snapshot.ts deleted file mode 100644 index e5c936b..0000000 --- a/apps/backend/src/handlers/dev/snapshot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Elysia } from "elysia"; -import { env } from "@cvsa/env"; -import v8 from "node:v8"; - -export const getHeapSnapshotHandler = new Elysia().get("/heap-snapshot", async ({ status }) => { - if (env.NODE_ENV !== "development") { - throw status(404); - } - const snapshotPath = v8.writeHeapSnapshot(); - return status(200, { - path: snapshotPath, - }); -}); diff --git a/apps/backend/src/handlers/index.ts b/apps/backend/src/handlers/index.ts deleted file mode 100644 index 95f13f5..0000000 --- a/apps/backend/src/handlers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./auth"; -export * from "./catalog/song"; -export * from "./catalog/engine"; -export * from "./catalog/artist"; -export * from "./catalog/artistRole"; -export * from "./catalog/singer"; -export * from "./dev"; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 4819fe7..fb3a3f7 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,4 +1,3 @@ -<<<<<<< HEAD import { Elysia, type ErrorHandler } from "elysia"; import { onAfterHandler } from "./onAfterHandle"; import { getBindingInfo, logStartup } from "./startMessage"; @@ -47,55 +46,6 @@ const errorHandler: ErrorHandler<{ message: "Internal server error", }); }; -======= -import { Elysia } from "elysia"; -import { onAfterHandler } from "./onAfterHandle"; -import { getBindingInfo, logStartup } from "./startMessage"; -import pkg from "../package.json"; -import { - authHandler, - songHandler, - engineHandler, - artistHandler, - artistRoleHandler, - singerHandler, -} from "@handlers/index"; -import { errorHandler } from "./errorHandler"; -import { openapi } from "@elysiajs/openapi"; -import { requestLoggerMiddleware } from "@/middlewares"; -import { opentelemetry } from "@elysiajs/opentelemetry"; -import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node"; -import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; -import { devHandler } from "./handlers"; -import { createOutboxWorker, closeOutboxInfrastructure } from "@cvsa/core"; -import { processOutboxEntry } from "@cvsa/core"; -import { outboxService } from "@cvsa/core"; -import { appLogger } from "@cvsa/logger"; - -const [host, port] = getBindingInfo(); - -logStartup(host, port); - -const outboxWorker = createOutboxWorker(processOutboxEntry); - -outboxService.recoverStaleEntries().catch((e) => { - appLogger.warn(`Failed to recover stale outbox entries: ${e.message}`); -}); - -process.on("SIGTERM", async () => { - appLogger.info("Received SIGTERM, shutting down gracefully..."); - await outboxWorker.close(); - await closeOutboxInfrastructure(); - process.exit(0); -}); - -process.on("SIGINT", async () => { - appLogger.info("Received SIGINT, shutting down gracefully..."); - await outboxWorker.close(); - await closeOutboxInfrastructure(); - process.exit(0); -}); ->>>>>>> origin/develop export const app = new Elysia({ serve: { @@ -103,31 +53,12 @@ export const app = new Elysia({ }, prefix: "/v2", }) -<<<<<<< HEAD .error({ AppError, }) .onError(errorHandler) .use(authHandler) .use(onAfterHandler) -======= - .use( - opentelemetry({ - spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())], - }) - ) - .use(onAfterHandler) - .use(requestLoggerMiddleware) - .use(errorHandler) - .use(openapi()) - .use(authHandler) - .use(songHandler) - .use(engineHandler) - .use(artistHandler) - .use(artistRoleHandler) - .use(singerHandler) - .use(devHandler) ->>>>>>> origin/develop .listen(16412); export const VERSION = pkg.version; diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts deleted file mode 100644 index c35eb19..0000000 --- a/apps/backend/src/middlewares/auth.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Elysia } from "elysia"; -import { AppError, auth, betterAuthUserToEntity, toBetterAuthHeaders } from "@cvsa/core"; -import { appLogger } from "@cvsa/logger"; - -export const authMiddleware = new Elysia({ name: "authMiddleware" }).derive( - { as: "scoped" }, - async ({ headers }) => { - const session = await auth.api.getSession({ - headers: toBetterAuthHeaders(headers), - }); - - if (!session) { - throw new AppError("error.unauthorized", "UNAUTHORIZED", 401); - } - - return { - session, - user: betterAuthUserToEntity(session.user), - }; - } -); diff --git a/apps/backend/src/middlewares/i18n.ts b/apps/backend/src/middlewares/i18n.ts deleted file mode 100644 index 45188bc..0000000 --- a/apps/backend/src/middlewares/i18n.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getTraceId } from "@/common/trace"; -import { Elysia } from "elysia"; - -const getLocale = (headers: Record) => { - if (headers["x-locale"]) { - return { - locale: headers["x-locale"], - }; - } - const acceptLang = headers["accept-language"]; - if (acceptLang) { - const langList = acceptLang.split(","); - for (const lang of langList) { - if (lang.includes("zh")) { - return { - locale: "zh", - }; - } else if (lang.includes("en")) { - return { - locale: "en", - }; - } - } - } - return { - locale: "en", - }; -}; - -export const i18nMiddleware = new Elysia({ name: "i18nMiddleware" }) - .state("locale", {} as Record) - .onError({ as: "global" }, ({ request, store }) => { - const traceId = getTraceId(); - const { locale } = getLocale(Object.fromEntries(request.headers.entries())); - if (traceId) { - store.locale = {}; - store.locale[traceId] = locale; - } - }) - .derive({ as: "scoped" }, ({ headers }) => { - return getLocale(headers); - }); diff --git a/apps/backend/src/middlewares/index.ts b/apps/backend/src/middlewares/index.ts deleted file mode 100644 index cb3c85a..0000000 --- a/apps/backend/src/middlewares/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./auth"; -export * from "./requestLogger"; -export * from "./i18n"; diff --git a/apps/backend/src/middlewares/requestLogger.ts b/apps/backend/src/middlewares/requestLogger.ts deleted file mode 100644 index 0286544..0000000 --- a/apps/backend/src/middlewares/requestLogger.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Elysia from "elysia"; -import type { Logger } from "pino"; -import { pinoLogger } from "@cvsa/logger"; -import { ip } from "elysia-ip"; -import { formatGinLog, getLogLevelForRequest } from "@common/log"; - -export interface RequestLoggerOptions { - excludePaths?: string[]; -} - -export function createRequestLoggerMiddleware(pinoLogger: Logger) { - return new Elysia() - .use(ip()) - .derive(function setRequestMeta() { - return { - startTime: Bun.nanoseconds() / 1_000_000, - }; - }) - .onAfterHandle({ as: "global" }, function afterResponseLog(context) { - const { request, ip, responseValue, startTime } = context; - const requestPath = new URL(request.url).pathname; - - const status = (() => { - if (!responseValue) { - return undefined; - } - const r = responseValue as { status?: number }; - if (r.status) { - return r.status; - } - const r2 = responseValue as { code?: number }; - if (r2.code) { - return r2.code; - } - return undefined; - })(); - - const userAgent = request.headers?.get("user-agent") ?? undefined; - - const logData: Record = { - type: "http_request", - method: request.method, - path: requestPath, - status, - latency: startTime - ? `${(Bun.nanoseconds() / 1_000_000 - startTime).toFixed(3)}ms` - : undefined, - userAgent, - ip, - }; - - pinoLogger[getLogLevelForRequest(status ?? 200)](formatGinLog(logData)); - }); -} - -export const requestLoggerMiddleware = createRequestLoggerMiddleware(pinoLogger); diff --git a/apps/backend/src/onAfterHandle.ts b/apps/backend/src/onAfterHandle.ts index bc32190..681d8bc 100644 --- a/apps/backend/src/onAfterHandle.ts +++ b/apps/backend/src/onAfterHandle.ts @@ -1,25 +1,10 @@ import Elysia, { ElysiaFile } from "elysia"; -<<<<<<< HEAD const encoder = new TextEncoder(); export const onAfterHandler = new Elysia().onAfterHandle( { as: "global" }, ({ responseValue, request }) => { -======= -import { getTraceId } from "@/common/trace"; - -const encoder = new TextEncoder(); - -export const onAfterHandler = new Elysia() - .onBeforeHandle({ as: "global" }, ({ set }) => { - const traceId = getTraceId(); - if (traceId) { - set.headers["X-Trace-ID"] = traceId; - } - }) - .onAfterHandle({ as: "global" }, ({ responseValue, request }) => { ->>>>>>> origin/develop const contentType = request.headers.get("Content-Type") || ""; const accept = request.headers.get("Accept") || ""; const secFetchMode = request.headers.get("Sec-Fetch-Mode"); @@ -33,21 +18,11 @@ export const onAfterHandler = new Elysia() if (responseValue instanceof ElysiaFile || responseValue instanceof Response) { return; } -<<<<<<< HEAD const realResponse = responseValue as { code?: number; response?: unknown }; if (realResponse.code) { const text = isBrowser ? JSON.stringify(realResponse.response, null, 2) : JSON.stringify(realResponse.response); -======= - if (responseValue === null) { - return; - } - const realResponse = responseValue as { code?: number; response?: unknown }; - - if (realResponse.code && isBrowser) { - const text = JSON.stringify(realResponse.response, null, 4); ->>>>>>> origin/develop return new Response(encoder.encode(text), { headers: { "Content-Type": "application/json; charset=utf-8", @@ -55,7 +30,6 @@ export const onAfterHandler = new Elysia() status: realResponse.code, }); } -<<<<<<< HEAD const text = isBrowser ? JSON.stringify(realResponse, null, 2) : JSON.stringify(realResponse); @@ -66,15 +40,3 @@ export const onAfterHandler = new Elysia() }); } ); -======= - if (isBrowser) { - const text = JSON.stringify(realResponse, null, 4); - - return new Response(encoder.encode(text), { - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); - } - }); ->>>>>>> origin/develop diff --git a/apps/backend/src/startMessage.ts b/apps/backend/src/startMessage.ts index 00740fc..1aa2370 100644 --- a/apps/backend/src/startMessage.ts +++ b/apps/backend/src/startMessage.ts @@ -1,8 +1,4 @@ -<<<<<<< HEAD import { env } from "@lib/env"; -======= -import { env } from "@cvsa/env"; ->>>>>>> origin/develop import chalk from "chalk"; import os from "node:os"; diff --git a/apps/backend/tests/artist.test.ts b/apps/backend/tests/artist.test.ts deleted file mode 100644 index 36a1841..0000000 --- a/apps/backend/tests/artist.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Artist E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/artist/:id - Get Artist Details", () => { - test("should retrieve an artist", async () => { - const artist = await prisma.artist.create({ - data: { - name: "Test Artist", - description: "Test Description", - language: "zh", - aliases: ["Alias1", "Alias2"], - }, - }); - - const { data, status } = await api.v2.artist({ id: artist.id }).get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Test Artist", - description: "Test Description", - language: "zh", - aliases: ["Alias1", "Alias2"], - }); - }); - - test("should return 404 for nonexistent artist", async () => { - const { error, status } = await api.v2.artist({ id: 99999 }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should not retrieve soft-deleted artist", async () => { - const artist = await prisma.artist.create({ - data: { - name: "Deleted Artist", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2.artist({ id: artist.id }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/artist - Create Artist", () => { - test("should create an artist with authentication", async () => { - const token = await getAuthToken(); - - const payload = { - name: "New Artist", - description: "A talented producer", - language: "zh", - aliases: ["Producer X", "PX"], - }; - - const { data, status } = await api.v2.artist.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "New Artist", - description: "A talented producer", - language: "zh", - aliases: ["Producer X", "PX"], - }); - }); - - test("should return 401 without authentication", async () => { - const payload = { - name: "Test Artist", - }; - - const { error, status } = await api.v2.artist.post(payload); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should create an artist with localized names and descriptions", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Artist Name", - localizedNames: { - zh: "艺术家名称", - ja: "アーティスト名", - }, - description: "Description in English", - localizedDescriptions: { - zh: "中文描述", - ja: "日本語の説明", - }, - language: "zh", - }; - - const { data, status } = await api.v2.artist.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Artist Name", - localizedNames: { - zh: "艺术家名称", - ja: "アーティスト名", - }, - localizedDescriptions: { - zh: "中文描述", - ja: "日本語の説明", - }, - }); - }); - - test("should create an artist with minimal fields", async () => { - const token = await getAuthToken(); - - const { data, status } = await api.v2.artist.post( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - }); - }); - }); - - describe("PATCH /v2/artist/:id - Update Artist", () => { - test("should update an artist with authentication", async () => { - const token = await getAuthToken(); - - const artist = await prisma.artist.create({ - data: { - name: "Original Name", - description: "Original Description", - language: "zh", - }, - }); - - const { data, status } = await api.v2.artist({ id: artist.id }).patch( - { name: "Updated Name", description: "Updated Description", language: "en" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.name).toBe("Updated Name"); - expect(data?.description).toBe("Updated Description"); - expect(data?.language).toBe("en"); - }); - - test("should return 401 without authentication", async () => { - const artist = await prisma.artist.create({ - data: { - name: "Test Artist", - }, - }); - - const { status } = await api.v2.artist({ id: artist.id }).patch({ - name: "Updated Name", - }); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent artist", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2.artist({ id: 999999 }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should update aliases array", async () => { - const token = await getAuthToken(); - - const artist = await prisma.artist.create({ - data: { - name: "Artist", - aliases: ["Old Alias"], - }, - }); - - const { data, status } = await api.v2.artist({ id: artist.id }).patch( - { aliases: ["New Alias 1", "New Alias 2"] }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.aliases).toEqual(["New Alias 1", "New Alias 2"]); - }); - }); - - describe("DELETE /v2/artist/:id - Delete Artist", () => { - test("should soft delete an artist with authentication", async () => { - const token = await getAuthToken(); - - const artist = await prisma.artist.create({ - data: { - name: "Artist to Delete", - }, - }); - - const { status } = await api.v2.artist({ id: artist.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedArtist = await prisma.artist.findUnique({ - where: { id: artist.id }, - }); - expect(deletedArtist?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const artist = await prisma.artist.create({ - data: { - name: "Test Artist", - }, - }); - - const { status } = await api.v2.artist({ id: artist.id }).delete({}, {}); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent artist", async () => { - const token = await getAuthToken(); - - const { status } = await api.v2.artist({ id: 999999 }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - }); - - test("should return 404 when trying to delete already deleted artist", async () => { - const token = await getAuthToken(); - - const artist = await prisma.artist.create({ - data: { - name: "Already Deleted", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2.artist({ id: artist.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); -}); diff --git a/apps/backend/tests/artistRole.test.ts b/apps/backend/tests/artistRole.test.ts deleted file mode 100644 index 5ad0457..0000000 --- a/apps/backend/tests/artistRole.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Artist Role E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/artist-role/:id - Get Artist Role Details", () => { - test("should retrieve an artist role", async () => { - const role = await prisma.artistRole.create({ - data: { - name: "Producer", - localizedNames: { zh: "制作人", en: "Producer" }, - }, - }); - - const { data, status } = await api.v2["artist-role"]({ id: role.id }).get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Producer", - localizedNames: { zh: "制作人", en: "Producer" }, - }); - }); - - test("should return 404 for nonexistent role", async () => { - const { error, status } = await api.v2["artist-role"]({ id: 99999 }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should not retrieve soft-deleted role", async () => { - const role = await prisma.artistRole.create({ - data: { - name: "Deleted Role", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2["artist-role"]({ id: role.id }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/artist-role - Create Artist Role", () => { - test("should create a role with authentication", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Composer", - localizedNames: { zh: "作曲人", ja: "作曲家" }, - }; - - const { data, status } = await api.v2["artist-role"].post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Composer", - localizedNames: { zh: "作曲人", ja: "作曲家" }, - }); - }); - - test("should return 401 without authentication", async () => { - const payload = { - name: "Test Role", - }; - - const { error, status } = await api.v2["artist-role"].post(payload); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should create a role with minimal fields", async () => { - const token = await getAuthToken(); - - const { data, status } = await api.v2["artist-role"].post( - { name: "Illustrator" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Illustrator", - }); - }); - }); - - describe("PATCH /v2/artist-role/:id - Update Artist Role", () => { - test("should update a role with authentication", async () => { - const token = await getAuthToken(); - - const role = await prisma.artistRole.create({ - data: { - name: "Original Name", - }, - }); - - const { data, status } = await api.v2["artist-role"]({ id: role.id }).patch( - { - name: "Updated Name", - localizedNames: { zh: "更新名称" }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.name).toBe("Updated Name"); - expect(data?.localizedNames).toEqual({ zh: "更新名称" }); - }); - - test("should return 401 without authentication", async () => { - const role = await prisma.artistRole.create({ - data: { - name: "Test Role", - }, - }); - - const { status } = await api.v2["artist-role"]({ id: role.id }).patch({ - name: "Updated Name", - }); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent role", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2["artist-role"]({ id: 999999 }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("DELETE /v2/artist-role/:id - Delete Artist Role", () => { - test("should soft delete a role with authentication", async () => { - const token = await getAuthToken(); - - const role = await prisma.artistRole.create({ - data: { - name: "Role to Delete", - }, - }); - - const { status } = await api.v2["artist-role"]({ id: role.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedRole = await prisma.artistRole.findUnique({ - where: { id: role.id }, - }); - expect(deletedRole?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const role = await prisma.artistRole.create({ - data: { - name: "Test Role", - }, - }); - - const { status } = await api.v2["artist-role"]({ id: role.id }).delete({}, {}); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent role", async () => { - const token = await getAuthToken(); - - const { status } = await api.v2["artist-role"]({ id: 999999 }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - }); - - test("should return 404 when trying to delete already deleted role", async () => { - const token = await getAuthToken(); - - const role = await prisma.artistRole.create({ - data: { - name: "Already Deleted", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2["artist-role"]({ id: role.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); -}); diff --git a/apps/backend/tests/auth.test.ts b/apps/backend/tests/auth.test.ts deleted file mode 100644 index 6ee39aa..0000000 --- a/apps/backend/tests/auth.test.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; - -const api = treaty(app); - -describe("Registration E2E Tests - POST /v2/user", () => { - test("should register a new user", async () => { - const payload = { - username: "new_user", - password: "password123", - email: `${Math.random()}@example.com`, - }; - - const { data, status } = await api.v2.user.post(payload); - - expect(status).toBe(200); - expect(data).toMatchObject({ - message: expect.any(String), - data: { - id: expect.any(String), - username: payload.username, - displayName: expect.any(String), - email: payload.email, - token: expect.any(String), - }, - }); - }); - - test("should fail when nothing is provided", async () => { - // Create the first user - // @ts-expect-error it's intended - const { error, status } = await api.v2.user.post({}); - - expect(status).toBe(422); - expect(error?.value).toMatchObject({ - code: "VALIDATION_ERROR", - }); - }); - - test("should fail when username already exists", async () => { - const payload = { - username: "duplicate_user", - password: "password123", - email: "01_first@example.com", - }; - - // Create the first user - await api.v2.user.post(payload); - - // Attempt to register with the same username - const { error, status } = await api.v2.user.post({ - ...payload, - email: "01_second@example.com", - }); - - // Should return 409 Conflict - expect(status).toBe(409); - expect(error?.value).toMatchObject({ - code: "USERNAME_IS_ALREADY_TAKEN", - }); - }); - - test("should fail when email already exists", async () => { - const payload = { - username: "first_user", - password: "password123", - email: "duplicated@example.com", - }; - - // Create the first user - await api.v2.user.post(payload); - - // Attempt to register with the same username - const { error, status } = await api.v2.user.post({ - ...payload, - username: "another_user", - }); - - // Should return 409 Conflict - expect(status).toBe(409); - expect(error?.value).toMatchObject({ - code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", - }); - }); - - test("should fail with invalid email format", async () => { - const { status, error } = await api.v2.user.post({ - username: "valid_user", - password: "password123", - email: "invalid-email-format", // Invalid email - }); - - // Malformed token should be seen as unauthorized - expect(status).toBe(422); - expect(error?.value).toMatchObject({ - code: "VALIDATION_ERROR", - }); - }); -}); - -describe("Profile E2E Tests - GET /v2/me", () => { - test("should return user profile with valid token", async () => { - // 1. Register to get a token - const signup = await api.v2.user.post({ - username: "profile_user", - password: "password123", - email: "first@example.com", - displayName: "John Doe", - }); - - const token = signup.data?.data.token; - expect(token).toBeDefined(); - - // 2. Fetch profile using the token in Authorization header - const { data, status } = await api.v2.me.get({ - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(200); - expect(data).toMatchObject({ - username: "profile_user", - email: "first@example.com", - displayName: "John Doe", - }); - expect(data?.id).toBeString(); - }); - - test("should return 401 for malformed token", async () => { - const { status, error } = await api.v2.me.get({ - headers: { - authorization: "Bearer invalid.token.format", - }, - }); - - // Malformed token should be seen as unauthorized - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should return 401 for non-existent session", async () => { - // Manually craft a token that looks valid but doesn't exist in DB - // Format: hex(30).hex(30) - const fakeToken = "0123456789abcdef0123456789abcd.0123456789abcdef0123456789abcd"; - - const { status } = await api.v2.me.get({ - headers: { - authorization: `Bearer ${fakeToken}`, - }, - }); - - expect(status).toBe(401); - }); - - test("should return 401 when Authorization header is missing", async () => { - const { status } = await api.v2.me.get(); - - expect(status).toBe(401); - }); -}); - -describe("Login E2E Tests - POST /v2/session", () => { - test("should login with valid credentials", async () => { - const signupPayload = { - username: "login_user", - password: "password123", - email: "login@example.com", - }; - - await api.v2.user.post(signupPayload); - - const { data, status } = await api.v2.session.post({ - email: signupPayload.email, - password: signupPayload.password, - }); - - expect(status).toBe(200); - expect(data).toMatchObject({ - message: "Successfully logged in", - data: { - id: expect.any(String), - username: signupPayload.username, - email: signupPayload.email, - token: expect.any(String), - }, - }); - }); - - test("should return 401 on invalid email", async () => { - const signupPayload = { - username: "user_invalid_email", - password: "password123", - email: "invalid@example.com", - }; - - await api.v2.user.post(signupPayload); - - const { error, status } = await api.v2.session.post({ - email: "nonexistent@example.com", - password: signupPayload.password, - }); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "INVALID_CREDENTIALS", - }); - }); - - test("should return 401 on invalid password", async () => { - const signupPayload = { - username: "user_invalid_pass", - password: "password123", - email: "wrongpass@example.com", - }; - - await api.v2.user.post(signupPayload); - - const { error, status } = await api.v2.session.post({ - email: signupPayload.email, - password: "wrongpassword", - }); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "INVALID_CREDENTIALS", - }); - }); - - test("should return 422 on missing fields", async () => { - // @ts-expect-error it's intended - const { error, status } = await api.v2.session.post({}); - - expect(status).toBe(422); - expect(error?.value).toMatchObject({ - code: "VALIDATION_ERROR", - }); - }); -}); - -describe("Logout E2E Tests - DELETE /v2/session", () => { - test("should logout with valid token", async () => { - const signupPayload = { - username: "logout_user", - password: "password123", - email: "logout@example.com", - }; - - const { data: signUpData } = await api.v2.user.post(signupPayload); - - const { status } = await api.v2.session.delete(null, { - headers: { - authorization: `Bearer ${signUpData?.data.token}`, - }, - }); - - expect(status).toBe(204); - }); - - test("should return 401 on invalid/expired token", async () => { - const { error, status } = await api.v2.session.delete(null, { - headers: { - authorization: "Bearer invalid.token.format", - }, - }); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); -}); diff --git a/apps/backend/tests/engine.test.ts b/apps/backend/tests/engine.test.ts deleted file mode 100644 index 54f1f29..0000000 --- a/apps/backend/tests/engine.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Engine E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/engine/:id/details - Get Engine Details", () => { - test("should retrieve an engine", async () => { - const engine = await prisma.svsEngine.create({ - data: { - name: "Test Engine", - description: "Test Description", - }, - }); - - const { data, status } = await api.v2.engine({ id: engine.id }).details.get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Test Engine", - description: "Test Description", - }); - }); - - test("should return 404 for nonexistent engine", async () => { - const { error, status } = await api.v2.engine({ id: 99999 }).details.get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/engine - Create Engine", () => { - test("should create an engine with authentication", async () => { - const token = await getAuthToken(); - - const payload = { - name: "VOCALOID", - description: "A singing voice synthesis software", - }; - - const { data, status } = await api.v2.engine.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "VOCALOID", - description: "A singing voice synthesis software", - }); - }); - - test("should return 401 without authentication", async () => { - const payload = { - name: "Test Engine", - }; - - const { error, status } = await api.v2.engine.post(payload); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should create an engine with localized descriptions", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Synthesizer V", - description: "A singing voice synthesis software", - localizedDescriptions: { - zh: "歌声合成软件", - ja: "歌声合成ソフトウェア", - }, - }; - - const { data, status } = await api.v2.engine.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Synthesizer V", - localizedDescriptions: { - zh: "歌声合成软件", - ja: "歌声合成ソフトウェア", - }, - }); - }); - }); - - describe("PATCH /v2/engine/:id - Update Engine", () => { - test("should update an engine with authentication", async () => { - const token = await getAuthToken(); - - const engine = await prisma.svsEngine.create({ - data: { - name: "Original Name", - description: "Original Description", - }, - }); - - const { data, status } = await api.v2.engine({ id: engine.id }).patch( - { name: "Updated Name", description: "Updated Description" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.name).toBe("Updated Name"); - expect(data?.description).toBe("Updated Description"); - }); - - test("should return 401 without authentication", async () => { - const engine = await prisma.svsEngine.create({ - data: { - name: "Test Engine", - }, - }); - - const { status } = await api.v2.engine({ id: engine.id }).patch({ - name: "Updated Name", - }); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent engine", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2.engine({ id: 999999 }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("DELETE /v2/engine/:id - Delete Engine", () => { - test("should soft delete an engine with authentication", async () => { - const token = await getAuthToken(); - - const engine = await prisma.svsEngine.create({ - data: { - name: "Engine to Delete", - }, - }); - - const { status } = await api.v2.engine({ id: engine.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedEngine = await prisma.svsEngine.findUnique({ - where: { id: engine.id }, - }); - expect(deletedEngine?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const engine = await prisma.svsEngine.create({ - data: { - name: "Test Engine", - }, - }); - - const { status } = await api.v2.engine({ id: engine.id }).delete({}, {}); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent engine", async () => { - const token = await getAuthToken(); - - const { status } = await api.v2.engine({ id: 999999 }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - }); - }); -}); diff --git a/apps/backend/tests/singer.test.ts b/apps/backend/tests/singer.test.ts deleted file mode 100644 index ace461f..0000000 --- a/apps/backend/tests/singer.test.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Singer E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/singer/:id - Get Singer", () => { - test("should retrieve a singer", async () => { - const singer = await prisma.singer.create({ - data: { - name: "Test Singer", - description: "Test Description", - language: "zh", - }, - }); - - const { data, status } = await api.v2.singer({ id: singer.id }).get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Test Singer", - description: "Test Description", - language: "zh", - }); - }); - - test("should return 404 for nonexistent singer", async () => { - const { error, status } = await api.v2.singer({ id: 99999 }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should not retrieve soft-deleted singer", async () => { - const singer = await prisma.singer.create({ - data: { - name: "Deleted Singer", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2.singer({ id: singer.id }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/singer - Create Singer", () => { - test("should create a singer with authentication", async () => { - const token = await getAuthToken(); - - const payload = { - name: "New Singer", - description: "A virtual singer", - language: "zh", - }; - - const { data, status } = await api.v2.singer.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "New Singer", - description: "A virtual singer", - language: "zh", - }); - }); - - test("should return 401 without authentication", async () => { - const payload = { - name: "Test Singer", - }; - - const { error, status } = await api.v2.singer.post(payload); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should create a singer with localized names and descriptions", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Singer Name", - localizedNames: { - zh: "歌手名称", - ja: "シンガー名", - }, - description: "Description in English", - localizedDescriptions: { - zh: "中文描述", - ja: "日本語の説明", - }, - language: "zh", - }; - - const { data, status } = await api.v2.singer.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Singer Name", - localizedNames: { - zh: "歌手名称", - ja: "シンガー名", - }, - localizedDescriptions: { - zh: "中文描述", - ja: "日本語の説明", - }, - }); - }); - - test("should create a singer with minimal fields", async () => { - const token = await getAuthToken(); - - const { data, status } = await api.v2.singer.post( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - }); - }); - }); - - describe("PATCH /v2/singer/:id - Update Singer", () => { - test("should update a singer with authentication", async () => { - const token = await getAuthToken(); - - const singer = await prisma.singer.create({ - data: { - name: "Original Name", - description: "Original Description", - language: "zh", - }, - }); - - const { data, status } = await api.v2.singer({ id: singer.id }).patch( - { name: "Updated Name", description: "Updated Description", language: "en" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.name).toBe("Updated Name"); - expect(data?.description).toBe("Updated Description"); - expect(data?.language).toBe("en"); - }); - - test("should return 401 without authentication", async () => { - const singer = await prisma.singer.create({ - data: { - name: "Test Singer", - }, - }); - - const { status } = await api.v2.singer({ id: singer.id }).patch({ - name: "Updated Name", - }); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent singer", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2.singer({ id: 999999 }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should update localized names", async () => { - const token = await getAuthToken(); - - const singer = await prisma.singer.create({ - data: { - name: "Singer", - localizedNames: { zh: "旧名" }, - }, - }); - - const { data, status } = await api.v2.singer({ id: singer.id }).patch( - { localizedNames: { zh: "新名", ja: "新しい名前" } }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.localizedNames).toEqual({ zh: "新名", ja: "新しい名前" }); - }); - }); - - describe("DELETE /v2/singer/:id - Delete Singer", () => { - test("should soft delete a singer with authentication", async () => { - const token = await getAuthToken(); - - const singer = await prisma.singer.create({ - data: { - name: "Singer to Delete", - }, - }); - - const { status } = await api.v2.singer({ id: singer.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedSinger = await prisma.singer.findUnique({ - where: { id: singer.id }, - }); - expect(deletedSinger?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const singer = await prisma.singer.create({ - data: { - name: "Test Singer", - }, - }); - - const { status } = await api.v2.singer({ id: singer.id }).delete({}, {}); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent singer", async () => { - const token = await getAuthToken(); - - const { status } = await api.v2.singer({ id: 999999 }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - }); - - test("should return 404 when trying to delete already deleted singer", async () => { - const token = await getAuthToken(); - - const singer = await prisma.singer.create({ - data: { - name: "Already Deleted", - deletedAt: new Date(), - }, - }); - - const { error, status } = await api.v2.singer({ id: singer.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); -}); diff --git a/apps/backend/tests/song.test.ts b/apps/backend/tests/song.test.ts deleted file mode 100644 index 43e9be3..0000000 --- a/apps/backend/tests/song.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Song E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/song/:id/details - Get Song Details", async () => { - test("should retrieve a song", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }, - }); - - const { data, status } = await api.v2.song({ id: song.id }).details.get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - }); - - test("should return 404 for nonexistent song", async () => { - const { error, status } = await api.v2.song({ id: 1099 }).details.get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/song - Create Song", () => { - test("should create a song with authentication", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Test Song", - type: "ORIGINAL" as const, - duration: 180, - }; - - const { data, status } = await api.v2.song.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - }); - - test("should return 401 without authentication", async () => { - const payload = { - name: "Test Song", - }; - - const { error, status } = await api.v2.song.post(payload); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should create a song with lyrics", async () => { - const token = await getAuthToken(); - - const payload = { - name: "Lyrics Song", - type: "ORIGINAL" as const, - lyrics: [ - { - language: "zh", - isTranslated: false, - plainText: "第一行歌词\n第二行歌词", - lrc: "[00:00.00]第一行歌词\n[00:05.00]第二行歌词", - ttml: "", - }, - ], - }; - - const { data, status } = await api.v2.song.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - name: "Lyrics Song", - }); - - // Verify lyrics were persisted to DB - const lyrics = await prisma.lyrics.findMany({ - where: { songId: data?.id }, - }); - expect(lyrics).toHaveLength(1); - expect(lyrics[0]).toMatchObject({ - language: "zh", - isTranslated: false, - plainText: "第一行歌词\n第二行歌词", - lrc: "[00:00.00]第一行歌词\n[00:05.00]第二行歌词", - ttml: "", - }); - }); - }); - - describe("PATCH /v2/song/:id - Update Song", () => { - test("should update a song with authentication", async () => { - const token = await getAuthToken(); - - const song = await prisma.song.create({ - data: { - name: "Original Name", - }, - }); - - const { data, status } = await api.v2.song({ id: song.id }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data?.name).toBe("Updated Name"); - }); - - test("should return 401 without authentication", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - }, - }); - - const { status } = await api.v2.song({ id: song.id }).patch({ - name: "Updated Name", - }); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent song", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2.song({ id: 999999 }).patch( - { name: "Updated Name" }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("DELETE /v2/song/:id - Delete Song", () => { - test("should soft delete a song with authentication", async () => { - const token = await getAuthToken(); - - const song = await prisma.song.create({ - data: { - name: "Song to Delete", - }, - }); - - const { status } = await api.v2.song({ id: song.id }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedSong = await prisma.song.findUnique({ - where: { id: song.id }, - }); - expect(deletedSong?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - }, - }); - - const { status } = await api.v2.song({ id: song.id }).delete({}, {}); - - expect(status).toBe(401); - }); - - test("should return 404 for non-existent song", async () => { - const token = await getAuthToken(); - - const { status } = await api.v2.song({ id: 999999 }).delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - }); - }); -}); diff --git a/apps/backend/tests/songLyrics.test.ts b/apps/backend/tests/songLyrics.test.ts deleted file mode 100644 index b55d728..0000000 --- a/apps/backend/tests/songLyrics.test.ts +++ /dev/null @@ -1,548 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "@/index"; -import { prisma } from "@cvsa/db"; - -const api = treaty(app); - -describe("Song Lyrics E2E Tests", () => { - async function getAuthToken() { - const signup = await api.v2.user.post({ - username: `${Math.random()}`, - password: "password123", - }); - return signup.data?.data.token; - } - - describe("GET /v2/song/:id/lyrics - List Song Lyrics", () => { - test("should return empty array when song has no lyrics", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); - - expect(status).toBe(200); - expect(data).toEqual([]); - }); - - test("should return all lyrics for a song", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "第一行歌词", - }, - { - language: "en", - isTranslated: true, - plainText: "First line lyrics", - }, - ], - }, - }, - }); - - const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); - - expect(status).toBe(200); - expect(data).toHaveLength(2); - expect(data?.[0]).toMatchObject({ - language: expect.any(String), - isTranslated: expect.any(Boolean), - }); - }); - - test("should return 404 for nonexistent song", async () => { - const { error, status } = await api.v2.song({ id: 99999 }).lyrics.get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should not return deleted lyrics", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "第一行歌词", - deletedAt: new Date(), - }, - { - language: "en", - isTranslated: true, - plainText: "First line lyrics", - }, - ], - }, - }, - }); - - const { data, status } = await api.v2.song({ id: song.id }).lyrics.get(); - - expect(status).toBe(200); - expect(data).toHaveLength(1); - expect(data?.[0].language).toBe("en"); - }); - }); - - describe("GET /v2/song/:id/lyric/:lyricId - Get Specific Lyric", () => { - test("should return a specific lyric", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "第一行歌词", - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { data, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .get(); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: lyrics.id, - language: "zh", - isTranslated: false, - plainText: "第一行歌词", - }); - }); - - test("should return 404 for nonexistent song", async () => { - const { error, status } = await api.v2.song({ id: 99999 }).lyric({ lyricId: 1 }).get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should return 404 for nonexistent lyric", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: 99999 }) - .get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should return 404 for deleted lyric", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "第一行歌词", - deletedAt: new Date(), - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .get(); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("POST /v2/song/:id/lyric - Create Lyric", () => { - test("should create a lyric with authentication", async () => { - const token = await getAuthToken(); - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const payload = { - language: "ja", - isTranslated: true, - plainText: "日本語の歌詞", - lrc: "[00:00.00]日本語の歌詞", - ttml: "日本語の歌詞", - }; - - const { data, status } = await api.v2.song({ id: song.id }).lyric.post(payload, { - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(status).toBe(201); - expect(data).toMatchObject({ - id: expect.any(Number), - language: "ja", - isTranslated: true, - plainText: "日本語の歌詞", - lrc: "[00:00.00]日本語の歌詞", - ttml: "日本語の歌詞", - }); - }); - - test("should return 401 without authentication", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const { error, status } = await api.v2.song({ id: song.id }).lyric.post({ - language: "zh", - plainText: "歌词", - }); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should return 404 for nonexistent song", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2.song({ id: 99999 }).lyric.post( - { - language: "zh", - plainText: "歌词", - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("PATCH /v2/song/:id/lyric/:lyricId - Update Lyric", () => { - test("should update a lyric with authentication", async () => { - const token = await getAuthToken(); - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "原歌词", - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { data, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .patch( - { - language: "en", - isTranslated: true, - plainText: "Updated lyrics", - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(200); - expect(data).toMatchObject({ - id: lyrics.id, - language: "en", - isTranslated: true, - plainText: "Updated lyrics", - }); - }); - - test("should return 401 without authentication", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "歌词", - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .patch({ - plainText: "Updated lyrics", - }); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should return 404 for nonexistent song", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2 - .song({ id: 99999 }) - .lyric({ lyricId: 1 }) - .patch( - { - plainText: "Updated lyrics", - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should return 404 for nonexistent lyric", async () => { - const token = await getAuthToken(); - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: 99999 }) - .patch( - { - plainText: "Updated lyrics", - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); - - describe("DELETE /v2/song/:id/lyric/:lyricId - Delete Lyric", () => { - test("should soft delete a lyric with authentication", async () => { - const token = await getAuthToken(); - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "歌词", - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(204); - - const deletedLyric = await prisma.lyrics.findUnique({ - where: { id: lyrics.id }, - }); - expect(deletedLyric?.deletedAt).not.toBeNull(); - }); - - test("should return 401 without authentication", async () => { - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - lyrics: { - create: [ - { - language: "zh", - isTranslated: false, - plainText: "歌词", - }, - ], - }, - }, - }); - - const lyrics = await prisma.lyrics.findFirst({ - where: { songId: song.id }, - }); - expect(lyrics).not.toBeNull(); - if (!lyrics) return; - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: lyrics.id }) - .delete({}, {}); - - expect(status).toBe(401); - expect(error?.value).toMatchObject({ - code: "UNAUTHORIZED", - }); - }); - - test("should return 404 for nonexistent song", async () => { - const token = await getAuthToken(); - - const { error, status } = await api.v2 - .song({ id: 99999 }) - .lyric({ lyricId: 1 }) - .delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - - test("should return 404 for nonexistent lyric", async () => { - const token = await getAuthToken(); - const song = await prisma.song.create({ - data: { - name: "Test Song", - type: "ORIGINAL", - }, - }); - - const { error, status } = await api.v2 - .song({ id: song.id }) - .lyric({ lyricId: 99999 }) - .delete( - {}, - { - headers: { - authorization: `Bearer ${token}`, - }, - } - ); - - expect(status).toBe(404); - expect(error?.value).toMatchObject({ - code: "NOT_FOUND", - }); - }); - }); -}); diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index f6947eb..d1d8541 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "extends": "@project-cvsa/configs/tsconfig/server.json", "compilerOptions": { "paths": { @@ -13,16 +12,4 @@ "@/*": ["./src/*"] }, } -======= - "extends": "@cvsa/typescript-config/base.json", - "compilerOptions": { - "paths": { - "@/*": ["./src/*"], - "@handlers/*": ["./src/handlers/*"], - "@common/*": ["./src/common/*"] - } - }, - "include": ["./src/**/*", "./tests/**/*"], - "exclude": ["../../packages/**/*"] ->>>>>>> origin/develop } diff --git a/apps/backend/turbo.json b/apps/backend/turbo.json deleted file mode 100644 index a576182..0000000 --- a/apps/backend/turbo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "lint": { - "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] - }, - "test": { - "dependsOn": ["typecheck"], - "inputs": [ - "tests/**.ts", - "src/**.ts", - "$TURBO_ROOT$/packages/db/**/*.ts", - "$TURBO_ROOT$/packages/db/**/*.prisma" - ] - }, - "typecheck": { - "inputs": [ - "tests/**.ts", - "src/**.ts", - "tsconfig.json", - "$TURBO_ROOT$/packages/typescript-config/base.json" - ] - } - } -} diff --git a/biome.json b/biome.json index bbe8bf0..29f9921 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", "root": true, "assist": { @@ -42,60 +41,4 @@ "enabled": true, "useIgnoreFile": true } -======= - "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", - "assist": { - "actions": { - "source": { - "useSortedKeys": "off", - "organizeImports": "off" - } - }, - "enabled": true - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "indentWidth": 4, - "lineWidth": 100, - "formatWithErrors": true - }, - "linter": { - "rules": { - "suspicious": { - "noExplicitAny": "error", - "noDeprecatedImports": "error" - }, - "nursery": {} - } - }, - "javascript": { - "formatter": { - "enabled": true, - "indentStyle": "tab", - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "es5" - } - }, - "json": { - "assist": { - "enabled": false - }, - "formatter": { - "enabled": true - }, - "linter": { - "enabled": false - } - }, - "vcs": { - "clientKind": "git", - "enabled": true, - "useIgnoreFile": true - }, - "files": { - "includes": ["**/*", "!.zed/*", "!packages/typescript-config/base.json", "!**/*.grit"] - } ->>>>>>> origin/develop } diff --git a/bun.lock b/bun.lock index 85f357b..822d287 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "": { "name": "cvsa", "devDependencies": { -<<<<<<< HEAD "turbo": "latest", }, }, @@ -19,51 +18,17 @@ "@project-cvsa/db": "workspace:*", "@t3-oss/env-core": "^0.13.10", "argon2": "^0.44.0", -======= - "@biomejs/biome": "^2.4.10", - "@types/bun": "^1.3.10", - "turbo": "^2.8.18", - "typescript": "^6.0.3", - }, - }, - "apps/backend": { - "name": "@cvsa/backend", - "version": "2.1.0", - "dependencies": { - "@better-auth/prisma-adapter": "^1.5.4", - "@cvsa/core": "workspace:*", - "@cvsa/db": "workspace:*", - "@cvsa/env": "workspace:*", - "@cvsa/logger": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "@elysiajs/openapi": "^1.4.14", - "@elysiajs/opentelemetry": "^1.4.10", - "@fanee/runtime-node": "^0.2.1", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", - "@opentelemetry/sdk-trace-node": "^2.6.1", - "@prisma/adapter-pg": "^7.4.1", - "@t3-oss/env-core": "^0.13.10", - "argon2": "^0.44.0", - "better-auth": "^1.5.4", ->>>>>>> origin/develop "chalk": "^5.6.2", "elysia": "^1.4.26", "elysia-ip": "^1.0.10", "elysia-rate-limit": "^4.5.0", "nanoid": "^5.1.6", -<<<<<<< HEAD -======= - "pino": "^10.3.1", ->>>>>>> origin/develop "zod": "^4.3.6", }, "devDependencies": { "@biomejs/biome": "^2.4.4", - "@cvsa/typescript-config": "workspace:*", "@types/bun": "latest", }, -<<<<<<< HEAD "peerDependencies": { "typescript": "^5", }, @@ -110,115 +75,14 @@ }, "devDependencies": { "prisma": "^7.4.1", -======= - }, - "packages/core": { - "name": "@cvsa/core", - "version": "0.1.0", - "dependencies": { - "@better-auth/prisma-adapter": "^1.5.6", - "@cvsa/db": "workspace:*", - "@cvsa/embedding": "workspace:*", - "@cvsa/env": "workspace:*", - "@cvsa/logger": "workspace:*", - "@cvsa/observability": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "better-auth": "^1.5.6", - "bullmq": "^5", - "ioredis": "^5", - "meilisearch": "^0.56.0", - "nanoid": "^5.1.7", - "remeda": "^2.33.7", - "zod": "^4.3.6", - }, - "devDependencies": { - "@cvsa/typescript-config": "workspace:*", - "@prisma/adapter-pg": "^7.4.1", - "@types/bun": "latest", - }, - }, - "packages/db": { - "name": "@cvsa/db", - "dependencies": { - "@cvsa/env": "workspace:*", - "@prisma/adapter-pg": "^7.6.0", - "@prisma/client": "^7.6.0", - "@types/pg": "^8.16.0", - "zod": "^4.3.6", - }, - "devDependencies": { - "@cvsa/prisma-zod": "^3.6.0", - "@cvsa/typescript-config": "workspace:*", - "prisma": "^7.6.0", ->>>>>>> origin/develop "prisma-json-types-generator": "^4.1.1", }, - }, - "packages/embedding": { - "name": "@cvsa/embedding", - "dependencies": { - "@cvsa/logger": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "@huggingface/hub": "^2.11.0", - "@huggingface/transformers": "^4.0.1", - "elysia": "latest", - "onnxruntime-node": "1.24.3", - "zod": "^4.3.6", - }, - "devDependencies": { - "@cvsa/typescript-config": "workspace:*", - "bun-types": "latest", - }, - }, - "packages/env": { - "name": "@cvsa/env", - "version": "0.1.0", - "dependencies": { - "@t3-oss/env-core": "^0.13.11", - "zod": "^4.3.6", - }, - "devDependencies": { - "@cvsa/typescript-config": "workspace:*", - "@types/bun": "latest", - }, - }, - "packages/logger": { - "name": "@cvsa/logger", - "version": "0.1.0", - "dependencies": { - "@cvsa/env": "workspace:*", - "@cvsa/observability": "workspace:*", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.214.0", - "pino": "^10.3.1", - "pino-pretty": "^13.1.3", - }, - "devDependencies": { - "@cvsa/typescript-config": "workspace:*", - "@types/bun": "latest", - }, - }, - "packages/observability": { - "name": "@cvsa/observability", - "version": "0.1.0", - "dependencies": { - "@cvsa/env": "workspace:*", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.214.0", - "@opentelemetry/resources": "^2.6.1", - "@opentelemetry/sdk-node": "^0.214.0", - }, - "devDependencies": { - "@cvsa/typescript-config": "workspace:*", - "@types/bun": "latest", + "peerDependencies": { + "typescript": "^5", }, }, - "packages/typescript-config": { - "name": "@cvsa/typescript-config", - }, }, "packages": { -<<<<<<< HEAD "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -282,329 +146,21 @@ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], -======= - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@better-auth/core": ["@better-auth/core@1.6.11", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-LrwidLCV8azdMGjvtwp30nj9tIv1BwI3VhtC0UaGSjQkAVWw4bN42I8qwbxRziPeSQoj+zUVkOpxZzAWBDARtQ=="], - - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-4jpkETIGZOHCf7BK4jnu22fdN6jjomH0/HhEzkaWy3+Eppi5PYlHTF/460jrTmA3Xc+Vqwp9t282ymHiEPypGw=="], - - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "kysely": "^0.28.17" }, "optionalPeers": ["kysely"] }, "sha512-/g8M9RfIjdcZDnbstSUvQiINkvdNlCeZr248zwqx2/PVksQI1MhQofbzUn3RnQnbPKp0EPwpX/dR3oudRFenUg=="], - - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0" } }, "sha512-hpdfw0BBf8MuzLkIdmbcUZICbY9r/bhLO2RxSnkzT5+/O+0I0u2I8+m0YUP7vNllP/ZCKASHOYgXPLO75Z0f9Q=="], - - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-3Tor8rSv8vSEIMEaV2PFpPEuVhqc1gNoZ6eGvoh3LwExXXuj8madew6ob+H1pH7Aphn3Ar5PQ08AguT8TbwFAA=="], - - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-Pw+7q7zTp+VSci1V+CYMvuxIbAeVMZLe4lRo46LJoAKMHfjFl5T/ycsyFvWs/DkWC7n9gZZzRDEbHp0I5FiKKw=="], - - "@better-auth/telemetry": ["@better-auth/telemetry@1.6.11", "", { "peerDependencies": { "@better-auth/core": "^1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-hsjDHc8MZbm6/AHeNdtywrWedXevnBjmdvnHTcZub+rTVjOv+Td0roI8USKuC6uUibmrl//2rJfVCsGbopihNA=="], - - "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - - "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="], - - "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], - - "@cvsa/backend": ["@cvsa/backend@workspace:apps/backend"], - - "@cvsa/core": ["@cvsa/core@workspace:packages/core"], - - "@cvsa/db": ["@cvsa/db@workspace:packages/db"], - - "@cvsa/embedding": ["@cvsa/embedding@workspace:packages/embedding"], - - "@cvsa/env": ["@cvsa/env@workspace:packages/env"], - - "@cvsa/logger": ["@cvsa/logger@workspace:packages/logger"], - - "@cvsa/observability": ["@cvsa/observability@workspace:packages/observability"], - - "@cvsa/prisma-zod": ["@cvsa/prisma-zod@3.6.0", "", { "dependencies": { "@prisma/generator-helper": "^7.5.0" }, "peerDependencies": { "@prisma/client": ">= 7.0.0", "prisma": ">= 7.0.0", "zod": ">= 4.0.0" }, "bin": { "prisma-zod": "dist/index.js" } }, "sha512-5/Vh93EqlaXwwJk9cudIclmZG8fX4XsQ2LiGSSN5JcszOzm/MPXuyuGJU9M70X1P+KAB8Vza8RsvXdXmTTJbSw=="], - - "@cvsa/typescript-config": ["@cvsa/typescript-config@workspace:packages/typescript-config"], - - "@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="], - - "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="], - - "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="], - - "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], - - "@elysiajs/openapi": ["@elysiajs/openapi@1.4.15", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-HDtGIrWfFJk6UJS7qbgRmjifbSqnnRGDM3CsU/GVJkxLbEVEkNuQDf+Quh9fGbytSrJ8g4+tX9eVjshYhCH29w=="], - - "@elysiajs/opentelemetry": ["@elysiajs/opentelemetry@1.4.11", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-node": "^0.200.0" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-GGggB40rXithCA9nSjSc25TFkBPeEEW3rD8me/TrRq4TejzWFJn5zEKdRMYWSpYBeiUsraCpQxu94K2ASz+UnA=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - - "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - - "@fanee/runtime-node": ["@fanee/runtime-node@0.2.1", "", { "dependencies": { "messageformat": "^4.0.0" } }, "sha512-txiKblLxjsO49SYeZDb9CClBp3EC7lp0kn7Zfm1CDT47nbrxIvUqupH/DOQbE0gjPIyv8MOkXZmjmBTORvxd7w=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], - - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@huggingface/blake3-jit": ["@huggingface/blake3-jit@0.0.2", "", {}, "sha512-Bq7B5qabyjrJfhBsl85Jd2QBtf+HzRD7h7A9GfN2lzrrsABhOa5evVPgzoCTxR7Ub0QFj7YDK1YkYRWBU25+2w=="], - - "@huggingface/hub": ["@huggingface/hub@2.12.0", "", { "dependencies": { "@huggingface/tasks": "^0.20.25", "@huggingface/xetchunk-wasm": "^0.0.6" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-PU+CBtOVsg6e35LkHBe5WjkIxwEhGkoh93ZLIi/J/zgvv4JhLmfwtQ8+t/HodibBzIIloC1jvaAMkJqDJaml1g=="], - - "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], - - "@huggingface/tasks": ["@huggingface/tasks@0.20.25", "", {}, "sha512-id1Yp40dLFh2Hqy6PFN+9+PEXyKCB13hq6JSe0AkHfq3XVGjQGEO8C0UFVFUlHWCzB7FgI+u/hXn6aeRs/SE9w=="], - - "@huggingface/tokenizers": ["@huggingface/tokenizers@0.1.3", "", {}, "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA=="], - - "@huggingface/transformers": ["@huggingface/transformers@4.2.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "@huggingface/tokenizers": "^0.1.3", "onnxruntime-node": "1.24.3", "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", "sharp": "^0.34.5" } }, "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ=="], - - "@huggingface/xetchunk-wasm": ["@huggingface/xetchunk-wasm@0.0.6", "", { "dependencies": { "@huggingface/blake3-jit": "0.0.2", "gearhash-jit": "1.0.2" } }, "sha512-LoYPl7jvUvOnkysrhMjAeBzK5mVe2GFu8O3IsZb1w7l7v+NqjDsyRduwct3yraPAGmzTxxdb/2aIORL98Y949g=="], - - "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], - - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - - "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], - - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - - "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], - - "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], - - "@opentelemetry/configuration": ["@opentelemetry/configuration@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-Q+awuEwxhETwIAXuxHvIY5ZMEP0ZqvxLTi9kclrkyVJppEUXYL3Bhiw3jYrxdHYMh0Y0tVInQH9FEZ1aMinvLA=="], - - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.7.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ=="], - - "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], - - "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw=="], - - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw=="], - - "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IWAVvCO1TlpotRjFmhQFz9RSfQy5BsLtDRBtptSrXZRwfyRPpuql/RMe5zdmu0Gxl3ERDFwOzOqkf3bwy7Jzcw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-0NGxWHVYHgbp51SEzmsP+Hdups81eRs229STcSWHo3WO0aqY6RpJ9csxfyEtFgaNrBDv6UfOh0je4ss/ROS6XA=="], - - "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA=="], - - "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pJIcghFGhx3VSCgP5U+yZx+OMNj0t+ttnhC8IjL5Wza7vWIczctF6t3AGcVQffi2dEqX+ZHANoBwoPR8y6RMKA=="], - - "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4TGYoZKebUWVuYkV6r5wS2dUF4zH7EbWFw/Uqz1ZM1tGHQeFT9wzHGXq3iSIXMUrwu5jRdxjfMaXrYejPu2kpQ=="], - - "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FWRZ7AWoTryYhthralHkfXUuyO3l7cRsnr49WcDio1orl2a7KxT8aDZdwQtV1adzoUvZ9Gfo+IstElghCS4zfw=="], - - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], - - "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg=="], - - "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-km2/hD3inLTqtLnUAHDGz7ZP/VOyZNslrC/iN66x4jkmpckwlONW54LRPNI6fm09/musDtZga9EWsxgwnjGUlw=="], - - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="], - - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], - - "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.214.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw=="], - - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], - - "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dvz9TA6cPqIbxolSzQ5x9br6iQlqdGhVYrm+oYc7pfJ7LaVXz8F0XIqhWbnKB5YvfZ6SUmabBUUxnvHs/9uhxA=="], - - "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-kKFMxBcjBZAC1vBch5mtZ/dJQvcAEKWga+c+q5iGgRLPIE6Mc649zEwMaCIQCzalziMJQiyUadFYMHmELB7AFw=="], - - "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], - - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], - - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], - - "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/configuration": "0.214.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.214.0", "@opentelemetry/exporter-logs-otlp-http": "0.214.0", "@opentelemetry/exporter-logs-otlp-proto": "0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.214.0", "@opentelemetry/exporter-prometheus": "0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "0.214.0", "@opentelemetry/exporter-zipkin": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/propagator-b3": "2.6.1", "@opentelemetry/propagator-jaeger": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-gl2XvQBJuPjhGcw9SsnQO5qxChAPMuGRPFaD8lqtF+Cey91NgGUQ0sio2vlDFOSm3JOLzc44vL+OAfx1dXuZjg=="], - - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], - - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.7.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg=="], - - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - - "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - - "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - - "@prisma/adapter-pg": ["@prisma/adapter-pg@7.8.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.8.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg=="], - - "@prisma/client": ["@prisma/client@7.8.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.8.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw=="], - - "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.8.0", "", {}, "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw=="], - - "@prisma/config": ["@prisma/config@7.8.0", "", { "dependencies": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw=="], - - "@prisma/debug": ["@prisma/debug@7.8.0", "", {}, "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA=="], - - "@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="], - - "@prisma/dmmf": ["@prisma/dmmf@7.8.0", "", {}, "sha512-7xzcSFWO6J+dFUgIX7jL7QqUhEDfaa8GSZGsjjHyZct1Su+6KrvMl3S2+fnRkuKUIoTPg3Mj02oZuUdaNSfsaw=="], - - "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A=="], - - "@prisma/engines": ["@prisma/engines@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/fetch-engine": "7.8.0", "@prisma/get-platform": "7.8.0" } }, "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw=="], - - "@prisma/engines-version": ["@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "", {}, "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA=="], - - "@prisma/fetch-engine": ["@prisma/fetch-engine@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/get-platform": "7.8.0" } }, "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ=="], - - "@prisma/generator": ["@prisma/generator@7.8.0", "", {}, "sha512-KHGB0b8/9pNWyiK9EPJNE2/v1bMtqJgJldqjNNVvoE4uOhNSSWTmhHhPVfRsiuOVybzHCdCUQ/gdidCbpYAD5w=="], - - "@prisma/generator-helper": ["@prisma/generator-helper@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/dmmf": "7.8.0", "@prisma/generator": "7.8.0" } }, "sha512-i+2Gad6D/0dS0YHKFdYX3M8KYN1gwNkET813WXKfW2HeWmgipmSJsNSzOA44kTM+Rx6Dev3yBQwx5sZXXdtgtQ=="], ->>>>>>> origin/develop "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], -<<<<<<< HEAD "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], -======= - "@prisma/streams-local": ["@prisma/streams-local@0.1.2", "", { "dependencies": { "ajv": "^8.12.0", "better-result": "^2.7.0", "env-paths": "^3.0.0", "proper-lockfile": "^4.1.2" } }, "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg=="], - - "@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], ->>>>>>> origin/develop "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], -<<<<<<< HEAD "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], -======= - "@t3-oss/env-core": ["@t3-oss/env-core@0.13.11", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], ->>>>>>> origin/develop "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], -<<<<<<< HEAD "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], @@ -612,237 +168,66 @@ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], -======= - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="], - - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw=="], - - "@turbo/linux-64": ["@turbo/linux-64@2.9.14", "", { "os": "linux", "cpu": "x64" }, "sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA=="], - - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g=="], - - "@turbo/windows-64": ["@turbo/windows-64@2.9.14", "", { "os": "win32", "cpu": "x64" }, "sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A=="], - - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], - - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], - - "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], - - "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], - - "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - - "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], - - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], - - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], ->>>>>>> origin/develop "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], -<<<<<<< HEAD "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], -======= - "better-auth": ["better-auth@1.6.11", "", { "dependencies": { "@better-auth/core": "1.6.11", "@better-auth/drizzle-adapter": "1.6.11", "@better-auth/kysely-adapter": "1.6.11", "@better-auth/memory-adapter": "1.6.11", "@better-auth/mongo-adapter": "1.6.11", "@better-auth/prisma-adapter": "1.6.11", "@better-auth/telemetry": "1.6.11", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-Wwt6+q07dwIhsp6XiM7L1qSXVUWBEtNl+eZvwM778CguFqDZFBN9Pt6LtFaHl55t8Z+Zc//5kxcbgDY8/79vFQ=="], - - "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], - - "better-result": ["better-result@2.9.2", "", {}, "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q=="], - - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - - "bullmq": ["bullmq@5.77.0", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", "msgpackr": "2.0.1", "node-abort-controller": "3.1.1", "semver": "7.8.0", "tslib": "2.8.1" }, "peerDependencies": { "redis": ">=5.0.0" }, "optionalPeers": ["redis"] }, "sha512-J8CBvBgov4hqj/5CipTLtgfINvNRDeTl9p2Et/Dah/KBY2ERcnMT1m3ZxW4R1QC68ZX6yQk5nONtslpbbTs2gw=="], - - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - - "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], ->>>>>>> origin/develop "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], -<<<<<<< HEAD "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], -======= - "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], - - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - - "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], - - "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], ->>>>>>> origin/develop "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], -<<<<<<< HEAD "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], -======= - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], - - "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], ->>>>>>> origin/develop "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], -<<<<<<< HEAD "@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], -======= - "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - - "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], ->>>>>>> origin/develop "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], -<<<<<<< HEAD "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], -======= - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], ->>>>>>> origin/develop "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@formatjs/fast-memoize": ["@formatjs/fast-memoize@3.1.0", "https://registry.npmmirror.com/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg=="], -<<<<<<< HEAD "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.8.1", "https://registry.npmmirror.com/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", { "dependencies": { "@formatjs/fast-memoize": "3.1.0", "tslib": "^2.8.1" } }, "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA=="], "@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.3", "https://registry.npmmirror.com/@fumadocs/tailwind/-/tailwind-0.0.3.tgz", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ=="], "@hono/node-server": ["@hono/node-server@1.19.9", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.9.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], -======= - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], - - "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - - "effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], - - "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], - - "elysia-ip": ["elysia-ip@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.0.9" } }, "sha512-c4wgDzKRTjkDDZqBS3U8t70FBXFIYS288viIX+WsePC3CH0v0SLnJJfqADYdCDFg+/NP2X/dH291AeVdFNEfWw=="], - - "elysia-rate-limit": ["elysia-rate-limit@4.6.2", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-3axf0dl9PECqg+Duo+qfiI/RWyYnl63osuVgaI4DnXJjLs7PxNzJINfnKIV1Tyg0mqONO1jzdAjwDrJ3HEmuug=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], ->>>>>>> origin/develop "@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], -<<<<<<< HEAD "@iconify/utils": ["@iconify/utils@3.1.0", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.0.tgz", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], -======= - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], ->>>>>>> origin/develop "@img/colour": ["@img/colour@1.1.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], -<<<<<<< HEAD "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], -======= - "fast-copy": ["fast-copy@4.0.3", "", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="], - - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - - "file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="], - - "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], ->>>>>>> origin/develop "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], -<<<<<<< HEAD "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], -======= - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gearhash-jit": ["gearhash-jit@1.0.2", "", {}, "sha512-UhzJL4KXSdqAKepy/tZwmi2Rcy0YMmtiC4DQS4SURCuIWdh8ECZtnXK2ePRMLigfB61hRKdLK/Vgg2bSw73izQ=="], - - "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], - - "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], - - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], ->>>>>>> origin/develop "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], @@ -850,19 +235,7 @@ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], -<<<<<<< HEAD "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], -======= - "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - - "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - - "hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="], ->>>>>>> origin/develop "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], @@ -870,123 +243,38 @@ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], -<<<<<<< HEAD "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], -======= - "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], - - "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], - - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], ->>>>>>> origin/develop "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], -<<<<<<< HEAD "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], -======= - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - - "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - - "kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - - "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], ->>>>>>> origin/develop "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], -<<<<<<< HEAD "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], -======= - "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], - - "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], - - "meilisearch": ["meilisearch@0.56.0", "", {}, "sha512-kBXba8DcSrLWHYqopCm2JL90oBy97VdIfHkP1ii7/eHufeEEEk9Zu5Vv/IFQpNzmhEpWszVFVlGvZoI6QJeGzA=="], - - "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], - - "messageformat": ["messageformat@4.0.0", "", {}, "sha512-XKmJ/ffTWToWOlHJzt85ZChQgVGC0LHzNWuNK8zuYpNySsB0nIEmytOdSAOW9ETKtkajAUJf520m5gFHHnrTYg=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - - "msgpackr": ["msgpackr@2.0.1", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], ->>>>>>> origin/develop "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], -<<<<<<< HEAD "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], -======= - "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], - - "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], - - "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], - - "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], - - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], ->>>>>>> origin/develop "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], -<<<<<<< HEAD "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], -======= - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onnxruntime-common": ["onnxruntime-common@1.24.3", "", {}, "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA=="], - - "onnxruntime-node": ["onnxruntime-node@1.24.3", "", { "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^3.0.0", "onnxruntime-common": "1.24.3" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg=="], - - "onnxruntime-web": ["onnxruntime-web@1.26.0-dev.20260416-b7804b056c", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], ->>>>>>> origin/develop "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], -<<<<<<< HEAD "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "https://registry.npmmirror.com/@mdx-js/mdx/-/mdx-3.1.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.1.0.tgz", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], @@ -996,51 +284,18 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@next/env": ["@next/env@16.1.6", "https://registry.npmmirror.com/@next/env/-/env-16.1.6.tgz", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], -======= - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], - - "pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], - - "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], - - "pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="], ->>>>>>> origin/develop "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], -<<<<<<< HEAD "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], -======= - "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], - - "pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="], ->>>>>>> origin/develop "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], -<<<<<<< HEAD "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], -======= - "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], - - "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], - - "pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="], - - "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], - - "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - - "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], ->>>>>>> origin/develop "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], @@ -1052,15 +307,10 @@ "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "https://registry.npmmirror.com/@oxc-project/runtime/-/runtime-0.115.0.tgz", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], -<<<<<<< HEAD "@oxc-project/types": ["@oxc-project/types@0.115.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.115.0.tgz", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], -======= - "prisma": ["prisma@7.8.0", "", { "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw=="], ->>>>>>> origin/develop "@phc/format": ["@phc/format@1.0.0", "https://registry.npmmirror.com/@phc/format/-/format-1.0.0.tgz", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], -<<<<<<< HEAD "@prisma/adapter-pg": ["@prisma/adapter-pg@7.4.2", "https://registry.npmmirror.com/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", { "dependencies": { "@prisma/driver-adapter-utils": "7.4.2", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ=="], "@prisma/client": ["@prisma/client@7.4.2", "https://registry.npmmirror.com/@prisma/client/-/client-7.4.2.tgz", { "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA=="], @@ -1076,87 +326,21 @@ "@prisma/dmmf": ["@prisma/dmmf@7.4.2", "https://registry.npmmirror.com/@prisma/dmmf/-/dmmf-7.4.2.tgz", {}, "sha512-EurUPjBwmAd69MM8PLZ5n94xeeQ+7SB3Wd9p2+YsgXhvqCa1cgJRBJiB6fTWkPTz5Lf05ySNJ/59EnuEJMAnrw=="], "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.4.2", "https://registry.npmmirror.com/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2" } }, "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw=="], -======= - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], - - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - - "protobufjs": ["protobufjs@7.6.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg=="], - - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], - - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - - "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], - - "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - - "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - - "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - - "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - - "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], - - "remeda": ["remeda@2.34.1", "", {}, "sha512-k5iIF3lHm2NQ+2bNGDvZTD5jZl/JZkCS6AQOfGjYBd7V4rbb3K5whHvab0/O7CqPI43vYzbnIVCQXQJqmOyI6w=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], - - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], ->>>>>>> origin/develop "@prisma/engines": ["@prisma/engines@7.4.2", "https://registry.npmmirror.com/@prisma/engines/-/engines-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/fetch-engine": "7.4.2", "@prisma/get-platform": "7.4.2" } }, "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA=="], -<<<<<<< HEAD "@prisma/engines-version": ["@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "https://registry.npmmirror.com/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", {}, "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg=="], -======= - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - - "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], ->>>>>>> origin/develop "@prisma/fetch-engine": ["@prisma/fetch-engine@7.4.2", "https://registry.npmmirror.com/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "@prisma/get-platform": "7.4.2" } }, "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA=="], -<<<<<<< HEAD "@prisma/generator": ["@prisma/generator@7.4.2", "https://registry.npmmirror.com/@prisma/generator/-/generator-7.4.2.tgz", {}, "sha512-+4B8Rrq5Ef2cm7cW7Wyu6oXn/QVcDVdNti9mKEkcbPVLlA85I6Rbd+j6r+k2E2nlj0zKZbDbq8fIx/Xnt1SmkA=="], -======= - "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - - "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], ->>>>>>> origin/develop "@prisma/generator-helper": ["@prisma/generator-helper@7.4.2", "https://registry.npmmirror.com/@prisma/generator-helper/-/generator-helper-7.4.2.tgz", { "dependencies": { "@prisma/debug": "7.4.2", "@prisma/dmmf": "7.4.2", "@prisma/generator": "7.4.2" } }, "sha512-PWTkqlMILGVghoWP8SZB3prw/EcthiyPYJsMoiVZrClqPMQn6doPjphCQffPDyGzZvAwmA64edbc6Gm/6Omkjg=="], -<<<<<<< HEAD "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "https://registry.npmmirror.com/@prisma/get-platform/-/get-platform-7.2.0.tgz", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], -======= - "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], - - "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], - - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], ->>>>>>> origin/develop "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "https://registry.npmmirror.com/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], -<<<<<<< HEAD "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "https://registry.npmmirror.com/@prisma/studio-core/-/studio-core-0.13.1.tgz", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], "@project-cvsa/backend": ["@project-cvsa/backend@workspace:apps/backend"], @@ -1168,35 +352,6 @@ "@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], -======= - "shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - - "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], - - "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - - "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], ->>>>>>> origin/develop "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], @@ -1204,29 +359,16 @@ "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], -<<<<<<< HEAD "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], -======= - "turbo": ["turbo@2.9.14", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.14", "@turbo/darwin-arm64": "2.9.14", "@turbo/linux-64": "2.9.14", "@turbo/linux-arm64": "2.9.14", "@turbo/windows-64": "2.9.14", "@turbo/windows-arm64": "2.9.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw=="], - - "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], ->>>>>>> origin/develop "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], -<<<<<<< HEAD "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], -======= - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], ->>>>>>> origin/develop "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], -<<<<<<< HEAD "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], @@ -1234,89 +376,13 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], -======= - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], - - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], - - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/sdk-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], - - "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - - "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], - - "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], - - "@prisma/dev/remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], - - "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], - - "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="], ->>>>>>> origin/develop "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], -<<<<<<< HEAD "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], -======= - "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.24.0-dev.20251116-b39e144322", "", {}, "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw=="], ->>>>>>> origin/develop "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], -<<<<<<< HEAD "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], @@ -2488,110 +1554,5 @@ "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], -======= - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/sdk-logs": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-grpc-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@3.0.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - - "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw=="], - - "@elysiajs/opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "@opentelemetry/sdk-node/@opentelemetry/instrumentation/require-in-the-middle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], ->>>>>>> origin/develop } } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 803ed39..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -services: - db: - image: postgres:18-alpine - container_name: cvsa-db - restart: always - ports: - - "127.0.0.1:5432:5432" - environment: - - PGDATA=/var/lib/postgresql/18/docker - env_file: - - .env.docker - volumes: - - data:/var/lib/postgresql - command: postgres - - meili: - image: getmeili/meilisearch:v1.40 - container_name: cvsa-meili - restart: always - ports: - - "127.0.0.1:7700:7700" - env_file: - - .env.docker - volumes: - - meili:/meili_data - - redis: - image: redis:7-alpine - container_name: cvsa-redis - restart: always - ports: - - "127.0.0.1:6379:6379" - volumes: - - redis:/data - -volumes: - data: - meili: - redis: diff --git a/locale/manifest.json b/locale/manifest.json deleted file mode 100644 index 206de88..0000000 --- a/locale/manifest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "format": "otb", - "specVersion": "0.3.0" -} diff --git a/locale/modules/backend/manifest.json b/locale/modules/backend/manifest.json deleted file mode 100644 index 206de88..0000000 --- a/locale/modules/backend/manifest.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "format": "otb", - "specVersion": "0.3.0" -} diff --git a/locale/modules/backend/messages/en.json b/locale/modules/backend/messages/en.json deleted file mode 100644 index d987e80..0000000 --- a/locale/modules/backend/messages/en.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "error.unauthorized": "Unauthorized", - "error.song.notfound": "This song does not exist.", - "error.lyric.notfound": "This lyric does not exist.", - "error.signup.failed": "Unable to sign up. Please try again in a moment.", - "error.login.failed": "Unable to log in. Please try again in a moment.", - "error.not-found": "The requested resource was not found.", - "error.invalid-cred": "Invalid Credentials", - "error.internal": "Internal Error", - "error.validation": "Invalid Input", - "error.username-taken": "This username is already taken.", - "error.email-taken": "This email is already registered.", - "error.artist.notfound": "This artist does not exist.", - "error.engine.notfound": "This engine does not exist." -} diff --git a/locale/modules/backend/messages/zh.json b/locale/modules/backend/messages/zh.json deleted file mode 100644 index 8159c29..0000000 --- a/locale/modules/backend/messages/zh.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "error.unauthorized": "未登录", - "error.song.notfound": "此歌曲不存在", - "error.lyric.notfound": "此歌词不存在", - "error.signup.failed": "很抱歉,暂时无法注册", - "error.login.failed": "很抱歉,暂时无法登录", - "error.not-found": "请求的路径不存在", - "error.invalid-cred": "提交的凭据无效", - "error.internal": "服务器内部错误", - "error.validation": "输入信息有误", - "error.username-taken": "此用户名已被注册", - "error.email-taken": "此邮箱已被注册", - "error.artist.notfound": "该创作者不存在", - "error.engine.notfound": "该引擎不存在" -} diff --git a/opencode.json b/opencode.json deleted file mode 100644 index 48c1311..0000000 --- a/opencode.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "bash": { - "git *": "ask", - "jj *": "allow", - "bun run test": "allow", - "bun typecheck": "allow", - "bun dev": "deny", - "bun run dev": "deny", - "bun lint": "allow" - } - } -} diff --git a/package.json b/package.json index 70a6add..2a72244 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "name": "cvsa", "private": true, "workspaces": [ @@ -18,32 +17,4 @@ "turbo": "latest" }, "packageManager": "bun@1.3.10" -======= - "name": "cvsa", - "private": true, - "workspaces": [ - "apps/*", - "packages/*" - ], - "scripts": { - "dev": "bunx --bun turbo run dev", - "build": "bunx --bun turbo run build", - "lint": "bunx --bun turbo run lint", - "lint:fix": "bunx --bun biome check --write --unsafe .", - "format": "bunx --bun biome format --write .", - "ci": "bunx --bun biome ci .", - "test": "bunx --bun turbo run test:reset && bunx --bun turbo run test", - "test:coverage": "bunx --bun turbo run test:reset && bunx --bun turbo run test:coverage", - "typecheck": "bunx --bun turbo typecheck", - "setup": "bunx --bun turbo setup", - "setup:ci": "bunx --bun turbo setup:ci" - }, - "devDependencies": { - "@biomejs/biome": "^2.4.10", - "@types/bun": "^1.3.10", - "turbo": "^2.8.18", - "typescript": "^6.0.3" - }, - "packageManager": "bun@1.3.12" ->>>>>>> origin/develop } diff --git a/packages/core/.env.example b/packages/core/.env.example deleted file mode 100644 index 5902192..0000000 --- a/packages/core/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa -MEILI_MASTER_KEY=0712041211241099 -MEILI_API_URL=http://127.0.0.1:7700 diff --git a/packages/core/bunfig.toml b/packages/core/bunfig.toml deleted file mode 100644 index b97d83e..0000000 --- a/packages/core/bunfig.toml +++ /dev/null @@ -1,15 +0,0 @@ -[test] -coveragePathIgnorePatterns = [ - "**/db/prisma/**", - "**/db/src/**", - "**/env/src/**", - "**/logger/src/**", - "**/modules/auth/**", - "src/error/**", - "**/**.interface.ts", - "src/utils/randomId.ts", - "src/utils/BaseRepository.ts", - "src/types/**", - "src/outbox/queue.ts", - "**/observability/src/**", -] \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index 2458fff..0000000 --- a/packages/core/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@cvsa/core", - "type": "module", - "main": "src/index.ts", - "module": "src/index.ts", - "version": "0.1.0", - "scripts": { - "lint": "bunx --bun biome lint", - "test": "bun test --concurrent", - "test:reset": "NODE_ENV=test bun scripts/cleanUpSearchIndex.ts", - "test:safe": "bun run test:reset && bun run test", - "test:coverage": "bun test --coverage", - "test:unit": "bun test tests/unit --concurrent", - "test:integration": "bun test tests/integration --concurrent", - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@prisma/adapter-pg": "^7.4.1", - "@types/bun": "latest", - "@cvsa/typescript-config": "workspace:*" - }, - "exports": { - ".": "./src/index.ts", - "./internal": "./src/internal.ts" - }, - "dependencies": { - "@better-auth/prisma-adapter": "^1.5.6", - "@cvsa/db": "workspace:*", - "@cvsa/embedding": "workspace:*", - "@cvsa/env": "workspace:*", - "@cvsa/logger": "workspace:*", - "@cvsa/observability": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "better-auth": "^1.5.6", - "bullmq": "^5", - "ioredis": "^5", - "meilisearch": "^0.56.0", - "nanoid": "^5.1.7", - "remeda": "^2.33.7", - "zod": "^4.3.6" - } -} diff --git a/packages/core/scripts/cleanUpSearchIndex.ts b/packages/core/scripts/cleanUpSearchIndex.ts deleted file mode 100644 index b6f1405..0000000 --- a/packages/core/scripts/cleanUpSearchIndex.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { searchManager } from "../src"; -import { INDEX_SETTINGS } from "../src/search/config"; - -await searchManager.clearAllIndex(); -const langs = ["en", "zh", "ja"]; -const keys = Object.keys(INDEX_SETTINGS); -for (const lang of langs) { - for (const key of keys) { - await searchManager.createIndex(`${key}_${lang}`); - } -} - -process.exit(0); diff --git a/packages/core/src/error/AppError.ts b/packages/core/src/error/AppError.ts deleted file mode 100644 index 06131da..0000000 --- a/packages/core/src/error/AppError.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const APP_ERROR_MAGIC_NUMBER = 0x0712_0412_1099; - -export class AppError extends Error { - private readonly __type = APP_ERROR_MAGIC_NUMBER; - - constructor( - public override message: string, - public code: string, - public statusCode: number = 500, - public meta?: Record - ) { - super(message); - this.name = new.target.name; - Error.captureStackTrace(this, new.target); - } - - static isAppError(obj: unknown): obj is AppError { - if (!obj || typeof obj !== "object") { - return false; - } - - if (obj instanceof AppError) { - return true; - } - - return "__type" in obj && (obj as { __type: unknown }).__type === APP_ERROR_MAGIC_NUMBER; - } -} diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts deleted file mode 100644 index e973307..0000000 --- a/packages/core/src/error/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./AppError"; -export * from "./schema"; diff --git a/packages/core/src/error/schema.ts b/packages/core/src/error/schema.ts deleted file mode 100644 index 939a7d7..0000000 --- a/packages/core/src/error/schema.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -const errorCodes = [ - "NOT_FOUND", - "VALIDATION_ERROR", - "ENTITY_CONFLICT", - "INVALID_CREDENTIALS", - "AUTH_ERROR", - "INTERNAL_SERVER_ERROR", - "USERNAME_IS_ALREADY_TAKEN", - "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", - "INVALID_EMAIL_OR_PASSWORD", - "USER_NOT_FOUND", - "EMAIL_NOT_VERIFIED", - "UNAUTHORIZED", -] as const; - -export const ErrorResponseSchema = z.object({ - code: z.enum(errorCodes), - message: z.string().optional(), - i18n: z.record(z.string(), z.string()).optional(), - traceId: z.string().optional(), -}); - -export type ErrorResponseDto = z.infer; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index a57b424..0000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./modules"; -export * from "./search"; -export * from "./utils"; -export * from "./types"; -export * from "./error"; -export * from "./outbox"; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts deleted file mode 100644 index 7001a41..0000000 --- a/packages/core/src/internal.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./modules"; -export * from "./search"; -export * from "./types"; -export * from "./utils"; -export * from "./error"; diff --git a/packages/core/src/modules/auth/betterAuth.ts b/packages/core/src/modules/auth/betterAuth.ts deleted file mode 100644 index 5334c89..0000000 --- a/packages/core/src/modules/auth/betterAuth.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { APIError, betterAuth } from "better-auth"; -import { prismaAdapter } from "@better-auth/prisma-adapter"; -import { prisma } from "@cvsa/db"; -import { getRandomId } from "@cvsa/core/internal"; -import { username } from "better-auth/plugins"; -import { bearer } from "better-auth/plugins"; -import { appLogger } from "@cvsa/logger"; - -const DAY = 24 * 60 * 60; -const HOUR = 60 * 60; - -export const auth = betterAuth({ - database: prismaAdapter(prisma, { provider: "postgresql" }), - user: { - additionalFields: { - reputation: { type: "number", defaultValue: 0 }, - roleId: { type: "number", required: false }, - }, - }, - password: { - hash: (password: string) => - Bun.password.hash(password, { - algorithm: "argon2id", - memoryCost: 4, - }), - verify: (password: string, hash: string) => Bun.password.verify(password, hash), - }, - session: { - expiresIn: 90 * DAY, - updateAge: 1 * DAY, - freshAge: 3 * HOUR, - deferSessionRefresh: true, - }, - advanced: { - database: { - generateId: (options) => { - if (options.model === "user" || options.model === "users") { - return getRandomId(7); - } - return getRandomId(12); - }, - }, - }, - emailAndPassword: { - enabled: true, - }, - plugins: [ - username({ - minUsernameLength: 1, - maxUsernameLength: 100, - // We don't have any limitations of username - usernameValidator: () => true, - }), - bearer(), - ], - logger: { - log: (level, message, ...args) => { - appLogger[level](message, { - metadata: args, - }); - }, - }, -}); - -export { APIError as BetterAuthAPIError }; diff --git a/packages/core/src/modules/auth/index.ts b/packages/core/src/modules/auth/index.ts deleted file mode 100644 index efe2ded..0000000 --- a/packages/core/src/modules/auth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./betterAuth"; -export * from "./user"; diff --git a/packages/core/src/modules/auth/user/dto.ts b/packages/core/src/modules/auth/user/dto.ts deleted file mode 100644 index e2ed638..0000000 --- a/packages/core/src/modules/auth/user/dto.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { z } from "zod"; -import { getRandomId } from "@cvsa/core/internal"; -import type { User } from "@cvsa/db"; -import type { auth } from "@cvsa/core/internal"; - -type BetterAuthUser = Exclude>, null>["user"]; - -type BetterAuthSignupRequestBody = Exclude< - Parameters[0], - undefined ->["body"]; - -export const SignupRequestSchema = z.object({ - username: z.string().min(1).max(100), - password: z.string().min(8), - displayName: z.string().min(1).max(100).optional().nullable(), - email: z.email().optional().nullable(), -}); - -export const SignupUserInfoSchema = z.object({ - id: z.string(), - username: z.string(), - displayName: z.string().optional().nullable(), - email: z.email().optional().nullable(), - token: z.string(), -}); - -export const CurrentUserInfoSchema = z.object({ - id: z.string(), - username: z.string(), - displayName: z.string().optional().nullable(), - email: z.email().optional().nullable(), - createdAt: z.iso.datetime().nullish(), - image: z.string().optional().nullable(), -}); - -export const SignupResponseSchema = z.object({ - message: z.string(), - data: SignupUserInfoSchema, -}); - -export type SignupRequestDto = z.infer; -export type SignupUserInfoDto = z.infer; -export type CurrentUserInfoDto = z.infer; -export type SignupResponseDto = z.infer; - -export function toBetterAuthHeaders( - headers: Record -): [string, string][] { - return Object.entries(headers).filter( - (entry): entry is [string, string] => entry[1] !== undefined - ); -} - -export function signupRequestToBetterAuth(body: SignupRequestDto): BetterAuthSignupRequestBody { - const fakeEmail = `delegate-${getRandomId(14).toLowerCase()}@projectcvsa.com`; - - return { - name: body.displayName || body.username, - email: body.email || fakeEmail, - password: body.password, - username: body.username, - }; -} - -export function betterAuthToSignupUserInfoDto( - user: BetterAuthUser, - token: string -): SignupUserInfoDto { - return { - id: user.id, - username: user.username ?? "", - displayName: user.name, - email: user.email, - token, - }; -} - -export function betterAuthToCurrentUserInfoDto(user: BetterAuthUser): CurrentUserInfoDto { - return { - id: user.id, - username: user.username ?? "", - displayName: user.name, - email: user.email, - createdAt: user.createdAt.toISOString() ?? null, - image: user.image ?? null, - }; -} - -export function toSignUpResponse(data: SignupUserInfoDto): SignupResponseDto { - return { - message: "Successfully registered", - data, - }; -} - -export const LoginRequestSchema = z.object({ - email: z.email(), - password: z.string().min(1), -}); - -export const LoginUserInfoSchema = z.object({ - id: z.string(), - username: z.string(), - displayName: z.string().optional().nullable(), - email: z.email().optional().nullable(), - token: z.string(), -}); - -export const LoginResponseSchema = z.object({ - message: z.string(), - data: LoginUserInfoSchema, -}); - -export type LoginRequestDto = z.infer; -export type LoginUserInfoDto = z.infer; -export type LoginResponseDto = z.infer; - -export function betterAuthToLoginUserInfoDto( - user: BetterAuthUser, - token: string -): LoginUserInfoDto { - return { - id: user.id, - username: user.username ?? "", - displayName: user.name, - email: user.email, - token, - }; -} - -export function toLoginResponse(data: LoginUserInfoDto): LoginResponseDto { - return { - message: "Successfully logged in", - data, - }; -} - -export const LogoutResponseSchema = z.object({ - message: z.string(), -}); - -export type LogoutResponseDto = z.infer; - -export const betterAuthUserToEntity = (user: BetterAuthUser): User => { - const { image, roleId } = user; - return { - ...user, - image: image ?? null, - roleId: roleId ?? null, - username: user.username ?? "", - displayUsername: user.displayUsername ?? "", - }; -}; diff --git a/packages/core/src/modules/auth/user/index.ts b/packages/core/src/modules/auth/user/index.ts deleted file mode 100644 index 0392b1b..0000000 --- a/packages/core/src/modules/auth/user/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dto"; diff --git a/packages/core/src/modules/catalog/artist/container.ts b/packages/core/src/modules/catalog/artist/container.ts deleted file mode 100644 index 6199de6..0000000 --- a/packages/core/src/modules/catalog/artist/container.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { ArtistRepository } from "./repository"; -import { ArtistService } from "./service"; -import { ArtistSearchService, searchManager } from "../../../search"; -import { treaty } from "@elysiajs/eden"; -import type { EmbeddingApp } from "@cvsa/embedding"; -import { outboxService } from "../../outbox/container"; - -const embeddingManager = treaty("localhost:14900"); - -export const artistRepository = new ArtistRepository(prisma); -export const artistSearchService = new ArtistSearchService( - artistRepository, - searchManager, - embeddingManager -); -export const artistService = new ArtistService(artistRepository, outboxService); diff --git a/packages/core/src/modules/catalog/artist/dto.ts b/packages/core/src/modules/catalog/artist/dto.ts deleted file mode 100644 index 2ac8d0d..0000000 --- a/packages/core/src/modules/catalog/artist/dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; -import { ArtistSchema, type Serialized } from "@cvsa/db"; - -export type ArtistId = number; - -export const CreateArtistRequestSchema = z.object({ - name: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), - language: z.string().optional(), - aliases: z.array(z.string()).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const UpdateArtistRequestSchema = z.object({ - name: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), - language: z.string().optional(), - aliases: z.array(z.string()).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const ArtistResponseSchema = ArtistSchema.omit({ deletedAt: true }); - -export const ArtistDetailsResponseSchema = ArtistResponseSchema; - -export type ArtistResponseDto = Serialized>; -export type ArtistDetailsResponseDto = Serialized>; -export type CreateArtistRequestDto = Serialized>; -export type UpdateArtistRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/artist/index.ts b/packages/core/src/modules/catalog/artist/index.ts deleted file mode 100644 index 132cf0c..0000000 --- a/packages/core/src/modules/catalog/artist/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./service"; -export * from "./repository.interface"; -export * from "./container"; diff --git a/packages/core/src/modules/catalog/artist/repository.interface.ts b/packages/core/src/modules/catalog/artist/repository.interface.ts deleted file mode 100644 index 4f286aa..0000000 --- a/packages/core/src/modules/catalog/artist/repository.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { - CreateArtistRequestDto, - ArtistId, - UpdateArtistRequestDto, - ArtistResponseDto, - ArtistDetailsResponseDto, -} from "./dto"; - -export abstract class IArtistRepository - implements IRepositoryWithGetDetails -{ - abstract getById(id: ArtistId, tx?: TxClient): Promise; - abstract getDetailsById(id: ArtistId, tx?: TxClient): Promise; - abstract create(input: CreateArtistRequestDto, tx?: TxClient): Promise; - abstract update( - id: ArtistId, - input: UpdateArtistRequestDto, - tx?: TxClient - ): Promise; - abstract softDelete(id: ArtistId, tx?: TxClient): Promise; -} diff --git a/packages/core/src/modules/catalog/artist/repository.ts b/packages/core/src/modules/catalog/artist/repository.ts deleted file mode 100644 index 279796d..0000000 --- a/packages/core/src/modules/catalog/artist/repository.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import type { TxClient } from "@cvsa/db"; -import { BaseRepository } from "../../../utils/BaseRepository"; -import type { - CreateArtistRequestDto, - ArtistId, - UpdateArtistRequestDto, - ArtistDetailsResponseDto, -} from "./dto"; -import type { IArtistRepository } from "./repository.interface"; - -export class ArtistRepository extends BaseRepository implements IArtistRepository { - constructor(private readonly prisma: PrismaClient) { - super(); - } - - async getById(id: ArtistId, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artist.getById", () => - client.artist.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async getDetailsById(id: ArtistId, tx?: TxClient): Promise { - const client = tx ?? this.prisma; - return this.query("db.artist.getDetailsById", () => - client.artist.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async create(input: CreateArtistRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artist.create", () => - client.artist.create({ - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async update(id: ArtistId, input: UpdateArtistRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artist.update", () => - client.artist.update({ - where: { id }, - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async softDelete(id: ArtistId, tx?: TxClient) { - const client = tx ?? this.prisma; - await this.query("db.artist.softDelete", () => - client.artist.update({ - where: { id }, - data: { deletedAt: new Date() }, - }) - ); - } -} diff --git a/packages/core/src/modules/catalog/artist/service.ts b/packages/core/src/modules/catalog/artist/service.ts deleted file mode 100644 index 854c873..0000000 --- a/packages/core/src/modules/catalog/artist/service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { OutboxService } from "../../outbox/service"; -import { AppError } from "../../../error/AppError"; -import type { IServiceWithGetDetails } from "../../../types/service"; -import { prisma } from "@cvsa/db"; -import type { - ArtistDetailsResponseDto, - ArtistId, - CreateArtistRequestDto, - UpdateArtistRequestDto, - ArtistResponseDto, -} from "./dto"; -import type { IArtistRepository } from "./repository.interface"; - -export class ArtistService implements IServiceWithGetDetails { - constructor( - private readonly repository: IArtistRepository, - private readonly outbox: OutboxService - ) {} - - async getDetails(id: ArtistId) { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.artist.notfound", "NOT_FOUND", 404); - } - return result; - } - - async create(input: CreateArtistRequestDto): Promise { - const { artist, entry } = await prisma.$transaction(async (tx) => { - const artist = await this.repository.create(input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "artist", - aggregateId: artist.id, - eventType: "artist.created", - }, - tx - ); - return { artist, entry }; - }); - - await this.outbox.enqueue(entry); - return artist; - } - - async update(id: ArtistId, input: UpdateArtistRequestDto): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.artist.notfound", "NOT_FOUND", 404); - } - - const { artist, entry } = await prisma.$transaction(async (tx) => { - const artist = await this.repository.update(id, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "artist", - aggregateId: id, - eventType: "artist.updated", - }, - tx - ); - return { artist, entry }; - }); - - await this.outbox.enqueue(entry); - return artist; - } - - async delete(id: ArtistId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.artist.notfound", "NOT_FOUND", 404); - } - - const entry = await prisma.$transaction(async (tx) => { - await this.repository.softDelete(id, tx); - return await this.outbox.createEntry( - { - aggregateType: "artist", - aggregateId: id, - eventType: "artist.deleted", - }, - tx - ); - }); - - await this.outbox.enqueue(entry); - } -} diff --git a/packages/core/src/modules/catalog/artistRole/container.ts b/packages/core/src/modules/catalog/artistRole/container.ts deleted file mode 100644 index 871702e..0000000 --- a/packages/core/src/modules/catalog/artistRole/container.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { ArtistRoleRepository } from "./repository"; -import { ArtistRoleService } from "./service"; -import { ArtistRoleSearchService, searchManager } from "../../../search"; -import { treaty } from "@elysiajs/eden"; -import type { EmbeddingApp } from "@cvsa/embedding"; -import { outboxService } from "../../outbox/container"; - -const embeddingManager = treaty("localhost:14900"); - -export const artistRoleRepository = new ArtistRoleRepository(prisma); -export const artistRoleSearchService = new ArtistRoleSearchService( - artistRoleRepository, - searchManager, - embeddingManager -); -export const artistRoleService = new ArtistRoleService(artistRoleRepository, outboxService); diff --git a/packages/core/src/modules/catalog/artistRole/dto.ts b/packages/core/src/modules/catalog/artistRole/dto.ts deleted file mode 100644 index 9fc45b4..0000000 --- a/packages/core/src/modules/catalog/artistRole/dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from "zod"; -import { ArtistRoleSchema, type Serialized } from "@cvsa/db"; - -export type ArtistRoleId = number; - -export const CreateArtistRoleRequestSchema = z.object({ - name: z.string(), - language: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), -}); - -export const UpdateArtistRoleRequestSchema = z.object({ - name: z.string().optional(), - language: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), -}); - -export const ArtistRoleResponseSchema = ArtistRoleSchema.omit({ deletedAt: true }); - -export const ArtistRoleDetailsResponseSchema = ArtistRoleResponseSchema; - -export type ArtistRoleResponseDto = Serialized>; -export type ArtistRoleDetailsResponseDto = Serialized< - z.infer ->; -export type CreateArtistRoleRequestDto = Serialized>; -export type UpdateArtistRoleRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/artistRole/index.ts b/packages/core/src/modules/catalog/artistRole/index.ts deleted file mode 100644 index 132cf0c..0000000 --- a/packages/core/src/modules/catalog/artistRole/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./service"; -export * from "./repository.interface"; -export * from "./container"; diff --git a/packages/core/src/modules/catalog/artistRole/repository.interface.ts b/packages/core/src/modules/catalog/artistRole/repository.interface.ts deleted file mode 100644 index 2d75397..0000000 --- a/packages/core/src/modules/catalog/artistRole/repository.interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { - CreateArtistRoleRequestDto, - ArtistRoleId, - UpdateArtistRoleRequestDto, - ArtistRoleResponseDto, - ArtistRoleDetailsResponseDto, -} from "./dto"; - -export abstract class IArtistRoleRepository - implements IRepositoryWithGetDetails -{ - abstract getById(id: ArtistRoleId, tx?: TxClient): Promise; - abstract getDetailsById( - id: ArtistRoleId, - tx?: TxClient - ): Promise; - abstract create( - input: CreateArtistRoleRequestDto, - tx?: TxClient - ): Promise; - abstract update( - id: ArtistRoleId, - input: UpdateArtistRoleRequestDto, - tx?: TxClient - ): Promise; - abstract softDelete(id: ArtistRoleId, tx?: TxClient): Promise; -} diff --git a/packages/core/src/modules/catalog/artistRole/repository.ts b/packages/core/src/modules/catalog/artistRole/repository.ts deleted file mode 100644 index b3b6998..0000000 --- a/packages/core/src/modules/catalog/artistRole/repository.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import type { TxClient } from "@cvsa/db"; -import { BaseRepository } from "../../../utils/BaseRepository"; -import type { - CreateArtistRoleRequestDto, - ArtistRoleId, - UpdateArtistRoleRequestDto, - ArtistRoleDetailsResponseDto, -} from "./dto"; -import type { IArtistRoleRepository } from "./repository.interface"; - -export class ArtistRoleRepository extends BaseRepository implements IArtistRoleRepository { - constructor(private readonly prisma: PrismaClient) { - super(); - } - - async getById(id: ArtistRoleId, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artistRole.getById", () => - client.artistRole.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async getDetailsById( - id: ArtistRoleId, - tx?: TxClient - ): Promise { - const client = tx ?? this.prisma; - return this.query("db.artistRole.getDetailsById", () => - client.artistRole.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async create(input: CreateArtistRoleRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artistRole.create", () => - client.artistRole.create({ - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async update(id: ArtistRoleId, input: UpdateArtistRoleRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.artistRole.update", () => - client.artistRole.update({ - where: { id }, - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async softDelete(id: ArtistRoleId, tx?: TxClient) { - const client = tx ?? this.prisma; - await this.query("db.artistRole.softDelete", () => - client.artistRole.update({ - where: { id }, - data: { deletedAt: new Date() }, - }) - ); - } -} diff --git a/packages/core/src/modules/catalog/artistRole/service.ts b/packages/core/src/modules/catalog/artistRole/service.ts deleted file mode 100644 index 67c031f..0000000 --- a/packages/core/src/modules/catalog/artistRole/service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { OutboxService } from "../../outbox/service"; -import { AppError } from "../../../error/AppError"; -import type { IServiceWithGetDetails } from "../../../types/service"; -import { prisma } from "@cvsa/db"; -import type { - ArtistRoleDetailsResponseDto, - ArtistRoleId, - CreateArtistRoleRequestDto, - UpdateArtistRoleRequestDto, - ArtistRoleResponseDto, -} from "./dto"; -import type { IArtistRoleRepository } from "./repository.interface"; - -export class ArtistRoleService implements IServiceWithGetDetails { - constructor( - private readonly repository: IArtistRoleRepository, - private readonly outbox: OutboxService - ) {} - - async getDetails(id: ArtistRoleId) { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); - } - return result; - } - - async create(input: CreateArtistRoleRequestDto): Promise { - const { role, entry } = await prisma.$transaction(async (tx) => { - const role = await this.repository.create(input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "artistRole", - aggregateId: role.id, - eventType: "artistRole.created", - }, - tx - ); - return { role, entry }; - }); - - await this.outbox.enqueue(entry); - return role; - } - - async update( - id: ArtistRoleId, - input: UpdateArtistRoleRequestDto - ): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); - } - - const { role, entry } = await prisma.$transaction(async (tx) => { - const role = await this.repository.update(id, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "artistRole", - aggregateId: id, - eventType: "artistRole.updated", - }, - tx - ); - return { role, entry }; - }); - - await this.outbox.enqueue(entry); - return role; - } - - async delete(id: ArtistRoleId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.artistRole.notfound", "NOT_FOUND", 404); - } - - const entry = await prisma.$transaction(async (tx) => { - await this.repository.softDelete(id, tx); - return await this.outbox.createEntry( - { - aggregateType: "artistRole", - aggregateId: id, - eventType: "artistRole.deleted", - }, - tx - ); - }); - - await this.outbox.enqueue(entry); - } -} diff --git a/packages/core/src/modules/catalog/engine/container.ts b/packages/core/src/modules/catalog/engine/container.ts deleted file mode 100644 index d66d88d..0000000 --- a/packages/core/src/modules/catalog/engine/container.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { EngineRepository } from "./repository"; -import { EngineService } from "./service"; - -export const engineRepository = new EngineRepository(prisma); -export const engineService = new EngineService(engineRepository); diff --git a/packages/core/src/modules/catalog/engine/dto.ts b/packages/core/src/modules/catalog/engine/dto.ts deleted file mode 100644 index e0e789e..0000000 --- a/packages/core/src/modules/catalog/engine/dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; -import { SvsEngineSchema, type Serialized } from "@cvsa/db"; - -export type EngineId = number; - -export const CreateEngineRequestSchema = z.object({ - name: z.string().min(1).max(255), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const UpdateEngineRequestSchema = z.object({ - name: z.string().min(1).max(255).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const EngineResponseSchema = SvsEngineSchema.omit({ deletedAt: true }); - -export const EngineDetailsResponseSchema = EngineResponseSchema; - -export type EngineResponseDto = Serialized>; -export type EngineDetailsResponseDto = Serialized>; -export type CreateEngineRequestDto = Serialized>; -export type UpdateEngineRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/engine/index.ts b/packages/core/src/modules/catalog/engine/index.ts deleted file mode 100644 index 132cf0c..0000000 --- a/packages/core/src/modules/catalog/engine/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./service"; -export * from "./repository.interface"; -export * from "./container"; diff --git a/packages/core/src/modules/catalog/engine/repository.interface.ts b/packages/core/src/modules/catalog/engine/repository.interface.ts deleted file mode 100644 index a106f1c..0000000 --- a/packages/core/src/modules/catalog/engine/repository.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { - CreateEngineRequestDto, - EngineId, - UpdateEngineRequestDto, - EngineResponseDto, - EngineDetailsResponseDto, -} from "./dto"; - -export abstract class IEngineRepository - implements IRepositoryWithGetDetails -{ - abstract getById(id: EngineId, tx?: TxClient): Promise; - abstract getDetailsById(id: EngineId, tx?: TxClient): Promise; - abstract create(input: CreateEngineRequestDto, tx?: TxClient): Promise; - abstract update( - id: EngineId, - input: UpdateEngineRequestDto, - tx?: TxClient - ): Promise; - abstract softDelete(id: EngineId, tx?: TxClient): Promise; -} diff --git a/packages/core/src/modules/catalog/engine/repository.ts b/packages/core/src/modules/catalog/engine/repository.ts deleted file mode 100644 index 27358f8..0000000 --- a/packages/core/src/modules/catalog/engine/repository.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import type { TxClient } from "@cvsa/db"; -import { BaseRepository } from "../../../utils/BaseRepository"; -import type { - CreateEngineRequestDto, - EngineId, - UpdateEngineRequestDto, - EngineDetailsResponseDto, -} from "./dto"; -import type { IEngineRepository } from "./repository.interface"; - -export class EngineRepository extends BaseRepository implements IEngineRepository { - constructor(private readonly prisma: PrismaClient) { - super(); - } - - async getById(id: EngineId, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.engine.getById", () => - client.svsEngine.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async getDetailsById(id: EngineId, tx?: TxClient): Promise { - const client = tx ?? this.prisma; - return this.query("db.engine.getDetailsById", () => - client.svsEngine.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async create(input: CreateEngineRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.engine.create", () => - client.svsEngine.create({ - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async update(id: EngineId, input: UpdateEngineRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.engine.update", () => - client.svsEngine.update({ - where: { id }, - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async softDelete(id: EngineId, tx?: TxClient) { - const client = tx ?? this.prisma; - await this.query("db.engine.softDelete", () => - client.svsEngine.update({ - where: { id }, - data: { deletedAt: new Date() }, - }) - ); - } -} diff --git a/packages/core/src/modules/catalog/engine/service.ts b/packages/core/src/modules/catalog/engine/service.ts deleted file mode 100644 index c463249..0000000 --- a/packages/core/src/modules/catalog/engine/service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AppError } from "../../../error/AppError"; -import type { IServiceWithGetDetails } from "../../../types/service"; -import type { - EngineDetailsResponseDto, - EngineId, - CreateEngineRequestDto, - UpdateEngineRequestDto, - EngineResponseDto, -} from "./dto"; -import type { IEngineRepository } from "./repository.interface"; -import { traceTask } from "@cvsa/observability"; - -export class EngineService implements IServiceWithGetDetails { - constructor(private readonly repository: IEngineRepository) {} - - async getDetails(id: EngineId) { - return traceTask("db findOne engine", async () => { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.engine.notfound", "NOT_FOUND", 404); - } - return result; - }); - } - - async create(input: CreateEngineRequestDto): Promise { - return traceTask("db create engine", async () => { - return await this.repository.create(input); - }); - } - - async update(id: EngineId, input: UpdateEngineRequestDto): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.engine.notfound", "NOT_FOUND", 404); - } - return traceTask("db update engine", async () => { - return await this.repository.update(id, input); - }); - } - - async delete(id: EngineId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.engine.notfound", "NOT_FOUND", 404); - } - await traceTask("db delete engine", async () => { - return await this.repository.softDelete(id); - }); - } -} diff --git a/packages/core/src/modules/catalog/index.ts b/packages/core/src/modules/catalog/index.ts deleted file mode 100644 index 1681e44..0000000 --- a/packages/core/src/modules/catalog/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./song"; -export * from "./engine"; -export * from "./artist"; -export * from "./artistRole"; -export * from "./singer"; diff --git a/packages/core/src/modules/catalog/singer/container.ts b/packages/core/src/modules/catalog/singer/container.ts deleted file mode 100644 index daccb49..0000000 --- a/packages/core/src/modules/catalog/singer/container.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { SingerRepository } from "./repository"; -import { SingerService } from "./service"; -import { SingerSearchService, searchManager } from "../../../search"; -import { treaty } from "@elysiajs/eden"; -import type { EmbeddingApp } from "@cvsa/embedding"; -import { outboxService } from "../../outbox/container"; - -const embeddingManager = treaty("localhost:14900"); - -export const singerRepository = new SingerRepository(prisma); -export const singerSearchService = new SingerSearchService( - singerRepository, - searchManager, - embeddingManager -); -export const singerService = new SingerService(singerRepository, outboxService); diff --git a/packages/core/src/modules/catalog/singer/dto.ts b/packages/core/src/modules/catalog/singer/dto.ts deleted file mode 100644 index 2ce21bf..0000000 --- a/packages/core/src/modules/catalog/singer/dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod"; -import { SingerSchema, type Serialized } from "@cvsa/db"; - -export type SingerId = number; - -export const CreateSingerRequestSchema = z.object({ - name: z.string().optional(), - avatarUrl: z.string().optional(), - language: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const UpdateSingerRequestSchema = z.object({ - name: z.string().optional(), - avatarUrl: z.string().optional(), - language: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), -}); - -export const SingerResponseSchema = SingerSchema.omit({ deletedAt: true }); - -export const SingerDetailsResponseSchema = SingerResponseSchema; - -export type SingerResponseDto = Serialized>; -export type SingerDetailsResponseDto = Serialized>; -export type CreateSingerRequestDto = Serialized>; -export type UpdateSingerRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/singer/index.ts b/packages/core/src/modules/catalog/singer/index.ts deleted file mode 100644 index 132cf0c..0000000 --- a/packages/core/src/modules/catalog/singer/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./service"; -export * from "./repository.interface"; -export * from "./container"; diff --git a/packages/core/src/modules/catalog/singer/repository.interface.ts b/packages/core/src/modules/catalog/singer/repository.interface.ts deleted file mode 100644 index a1c86ac..0000000 --- a/packages/core/src/modules/catalog/singer/repository.interface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { - CreateSingerRequestDto, - SingerId, - UpdateSingerRequestDto, - SingerResponseDto, - SingerDetailsResponseDto, -} from "./dto"; - -export abstract class ISingerRepository - implements IRepositoryWithGetDetails -{ - abstract getById(id: SingerId, tx?: TxClient): Promise; - abstract getDetailsById(id: SingerId, tx?: TxClient): Promise; - abstract create(input: CreateSingerRequestDto, tx?: TxClient): Promise; - abstract update( - id: SingerId, - input: UpdateSingerRequestDto, - tx?: TxClient - ): Promise; - abstract softDelete(id: SingerId, tx?: TxClient): Promise; -} diff --git a/packages/core/src/modules/catalog/singer/repository.ts b/packages/core/src/modules/catalog/singer/repository.ts deleted file mode 100644 index 7f8ce59..0000000 --- a/packages/core/src/modules/catalog/singer/repository.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import type { TxClient } from "@cvsa/db"; -import { BaseRepository } from "../../../utils/BaseRepository"; -import type { - CreateSingerRequestDto, - SingerId, - UpdateSingerRequestDto, - SingerDetailsResponseDto, -} from "./dto"; -import type { ISingerRepository } from "./repository.interface"; - -export class SingerRepository extends BaseRepository implements ISingerRepository { - constructor(private readonly prisma: PrismaClient) { - super(); - } - - async getById(id: SingerId, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.singer.getById", () => - client.singer.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async getDetailsById(id: SingerId, tx?: TxClient): Promise { - const client = tx ?? this.prisma; - return this.query("db.singer.getDetailsById", () => - client.singer.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async create(input: CreateSingerRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.singer.create", () => - client.singer.create({ - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async update(id: SingerId, input: UpdateSingerRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.singer.update", () => - client.singer.update({ - where: { id }, - data: input, - omit: { deletedAt: true }, - }) - ); - } - - async softDelete(id: SingerId, tx?: TxClient) { - const client = tx ?? this.prisma; - await this.query("db.singer.softDelete", () => - client.singer.update({ - where: { id }, - data: { deletedAt: new Date() }, - }) - ); - } -} diff --git a/packages/core/src/modules/catalog/singer/service.ts b/packages/core/src/modules/catalog/singer/service.ts deleted file mode 100644 index 8553395..0000000 --- a/packages/core/src/modules/catalog/singer/service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { OutboxService } from "../../outbox/service"; -import { AppError } from "../../../error/AppError"; -import type { IServiceWithGetDetails } from "../../../types/service"; -import { prisma } from "@cvsa/db"; -import type { - SingerDetailsResponseDto, - SingerId, - CreateSingerRequestDto, - UpdateSingerRequestDto, - SingerResponseDto, -} from "./dto"; -import type { ISingerRepository } from "./repository.interface"; - -export class SingerService implements IServiceWithGetDetails { - constructor( - private readonly repository: ISingerRepository, - private readonly outbox: OutboxService - ) {} - - async getDetails(id: SingerId) { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.singer.notfound", "NOT_FOUND", 404); - } - return result; - } - - async create(input: CreateSingerRequestDto): Promise { - const { singer, entry } = await prisma.$transaction(async (tx) => { - const singer = await this.repository.create(input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "singer", - aggregateId: singer.id, - eventType: "singer.created", - }, - tx - ); - return { singer, entry }; - }); - - await this.outbox.enqueue(entry); - return singer; - } - - async update(id: SingerId, input: UpdateSingerRequestDto): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.singer.notfound", "NOT_FOUND", 404); - } - - const { singer, entry } = await prisma.$transaction(async (tx) => { - const singer = await this.repository.update(id, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "singer", - aggregateId: id, - eventType: "singer.updated", - }, - tx - ); - return { singer, entry }; - }); - - await this.outbox.enqueue(entry); - return singer; - } - - async delete(id: SingerId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.singer.notfound", "NOT_FOUND", 404); - } - - const entry = await prisma.$transaction(async (tx) => { - await this.repository.softDelete(id, tx); - return await this.outbox.createEntry( - { - aggregateType: "singer", - aggregateId: id, - eventType: "singer.deleted", - }, - tx - ); - }); - - await this.outbox.enqueue(entry); - } -} diff --git a/packages/core/src/modules/catalog/song/container.ts b/packages/core/src/modules/catalog/song/container.ts deleted file mode 100644 index 86b9f0d..0000000 --- a/packages/core/src/modules/catalog/song/container.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { SongRepository } from "./repository"; -import { SongService } from "./service"; -import { SongSearchService, searchManager } from "../../../search"; -import { treaty } from "@elysiajs/eden"; -import type { EmbeddingApp } from "@cvsa/embedding"; -import { outboxService } from "../../outbox/container"; - -const embeddingManager = treaty("localhost:14900"); - -export const songRepository = new SongRepository(prisma); -export const songSearchService = new SongSearchService( - songRepository, - searchManager, - embeddingManager -); -export const songService = new SongService(songRepository, outboxService); diff --git a/packages/core/src/modules/catalog/song/dto.ts b/packages/core/src/modules/catalog/song/dto.ts deleted file mode 100644 index 6c1372d..0000000 --- a/packages/core/src/modules/catalog/song/dto.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { z } from "zod"; -import { - SongSchema, - SingerSchema, - ArtistSchema, - ArtistRoleSchema, - SongTypeSchema, - type Serialized, - LyricsSchema, -} from "@cvsa/db"; - -export type SongId = number; - -const CreateCreationSchema = z.object({ - artistId: z.int().positive(), - roleId: z.int().positive(), -}); - -const CreatePerformanceSchema = z.object({ - singerId: z.int().positive(), - voicebankId: z.int().positive().optional(), - svsEngineId: z.int().positive().optional(), - svsEngineVersionId: z.int().positive().optional(), -}); - -export const SongLyricsCreateRequestSchema = z.object({ - language: z.string().optional(), - isTranslated: z.boolean().optional(), - plainText: z.string().optional(), - ttml: z.string().optional(), - lrc: z.string().optional(), -}); - -export const SongLyricsUpdateRequestSchema = z.object({ - language: z.string().optional(), - isTranslated: z.boolean().optional(), - plainText: z.string().optional(), - ttml: z.string().optional(), - lrc: z.string().optional(), -}); - -export const CreateSongRequestSchema = z.object({ - name: z.string().optional(), - type: SongTypeSchema.optional(), - language: z.string().optional(), - localizedNames: z.record(z.string(), z.string()).optional(), - description: z.string().optional(), - localizedDescriptions: z.record(z.string(), z.string()).optional(), - duration: z.int().optional(), - coverUrl: z.url().optional(), - publishedAt: z.iso.datetime().optional(), - performances: z.array(CreatePerformanceSchema).optional(), - creations: z.array(CreateCreationSchema).optional(), - lyrics: z.array(SongLyricsCreateRequestSchema).optional(), -}); - -export const UpdateSongRequestSchema = z.object({ - name: z.string().optional(), - type: SongTypeSchema.optional(), - duration: z.int().optional(), - description: z.string().optional(), - coverUrl: z.url().optional(), - publishedAt: z.iso.datetime().optional(), -}); - -export const SongDetailsResponseSchema = z.intersection( - SongSchema.omit({ deletedAt: true }), - z.object({ - singers: z - .intersection( - SingerSchema.omit({ deletedAt: true }), - z.object({ - engine: z.string().nullable(), - }) - ) - .array(), - artists: z - .intersection( - ArtistSchema.omit({ deletedAt: true }), - z.object({ role: ArtistRoleSchema }) - ) - .array(), - lyrics: LyricsSchema.omit({ deletedAt: true }).array(), - }) -); - -export const SongResponseSchema = SongSchema.omit({ deletedAt: true }); - -export const SongLyricsResponseSchema = LyricsSchema.omit({ - songId: true, - deletedAt: true, -}); - -export const SongLyricsListResponseSchema = SongLyricsResponseSchema.array(); - -export type SongResponseDto = Serialized>; -export type CreateSongRequestDto = Serialized>; -export type UpdateSongRequestDto = Serialized>; -export type SongDetailsResponseDto = Serialized>; - -export type SongLyricsResponseDto = Serialized>; -export type SongLyricsListResponseDto = Serialized>; -export type SongLyricsCreateRequestDto = Serialized>; -export type SongLyricsUpdateRequestDto = Serialized>; diff --git a/packages/core/src/modules/catalog/song/index.ts b/packages/core/src/modules/catalog/song/index.ts deleted file mode 100644 index 132cf0c..0000000 --- a/packages/core/src/modules/catalog/song/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./service"; -export * from "./repository.interface"; -export * from "./container"; diff --git a/packages/core/src/modules/catalog/song/repository.interface.ts b/packages/core/src/modules/catalog/song/repository.interface.ts deleted file mode 100644 index be9083f..0000000 --- a/packages/core/src/modules/catalog/song/repository.interface.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { - CreateSongRequestDto, - SongId, - UpdateSongRequestDto, - SongDetailsResponseDto, - SongResponseDto, - SongLyricsCreateRequestDto, - SongLyricsUpdateRequestDto, - SongLyricsResponseDto, - SongLyricsListResponseDto, -} from "./dto"; - -export abstract class ISongRepository implements IRepositoryWithGetDetails { - abstract getById(id: SongId, tx?: TxClient): Promise; - abstract getDetailsById(id: SongId, tx?: TxClient): Promise; - abstract create(input: CreateSongRequestDto, tx?: TxClient): Promise; - abstract update( - id: SongId, - input: UpdateSongRequestDto, - tx?: TxClient - ): Promise; - abstract softDelete(id: SongId, tx?: TxClient): Promise; - abstract createLyrics( - id: SongId, - input: SongLyricsCreateRequestDto, - tx?: TxClient - ): Promise; - abstract getLyricsBySongId(id: SongId, tx?: TxClient): Promise; - abstract getLyricById(lyricId: number, tx?: TxClient): Promise; - abstract updateLyric( - lyricId: number, - input: SongLyricsUpdateRequestDto, - tx?: TxClient - ): Promise; - abstract softDeleteLyric(lyricId: number, tx?: TxClient): Promise; -} diff --git a/packages/core/src/modules/catalog/song/repository.ts b/packages/core/src/modules/catalog/song/repository.ts deleted file mode 100644 index 3de15aa..0000000 --- a/packages/core/src/modules/catalog/song/repository.ts +++ /dev/null @@ -1,198 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import type { TxClient } from "@cvsa/db"; -import { BaseRepository } from "../../../utils/BaseRepository"; -import type { - CreateSongRequestDto, - SongId, - UpdateSongRequestDto, - SongDetailsResponseDto, - SongLyricsCreateRequestDto, - SongLyricsUpdateRequestDto, -} from "./dto"; -import type { ISongRepository } from "./repository.interface"; - -export class SongRepository extends BaseRepository implements ISongRepository { - constructor(private readonly prisma: PrismaClient) { - super(); - } - - async getById(id: SongId, tx?: TxClient) { - const client = tx ?? this.prisma; - return this.query("db.song.getById", () => - client.song.findFirst({ - where: { id, deletedAt: null }, - omit: { deletedAt: true }, - }) - ); - } - - async getDetailsById(id: SongId, tx?: TxClient): Promise { - const client = tx ?? this.prisma; - const data = await this.query("db.song.getDetailsById", () => - client.song.findFirst({ - where: { id, deletedAt: null }, - include: { - performances: { - include: { - singer: true, - svsEngine: true, - svsEngineVersion: true, - }, - }, - creations: { - include: { - role: true, - artist: true, - }, - }, - lyrics: { - omit: { - deletedAt: true, - }, - }, - }, - omit: { - deletedAt: true, - }, - }) - ); - if (!data) return null; - const { performances, creations, lyrics, ...song } = data; - return { - singers: performances.map((item) => { - const engineName = item.svsEngine?.name ?? ""; - const engineVersion = item.svsEngineVersion?.versionString ?? ""; - const engine = (() => { - if (!engineName) return null; - if (!engineVersion) return engineName; - return `${engineName} ${engineVersion}`; - })(); - return { - ...item.singer, - engine, - }; - }), - artists: creations.map((item) => { - return { - ...item.artist, - role: item.role, - }; - }), - lyrics: lyrics, - ...song, - }; - } - - async create(input: CreateSongRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - const { performances, creations, lyrics, ...songData } = input; - - return this.query("db.song.create", () => - client.song.create({ - data: { - ...songData, - performances: performances && { - create: performances, - }, - creations: creations && { - create: creations.map((c) => ({ - artistId: c.artistId, - artistRoleId: c.roleId, - })), - }, - lyrics: lyrics && { - create: lyrics, - }, - }, - omit: { - deletedAt: true, - }, - }) - ); - } - - async update(id: SongId, input: UpdateSongRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - - return this.query("db.song.update", () => - client.song.update({ - where: { id }, - data: input, - omit: { - deletedAt: true, - }, - }) - ); - } - - async softDelete(id: SongId, tx?: TxClient) { - const client = tx ?? this.prisma; - await this.query("db.song.softDelete", () => - client.song.update({ - where: { id }, - data: { deletedAt: new Date() }, - }) - ); - } - - async createLyrics(id: SongId, input: SongLyricsCreateRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - - return this.query("db.song.createLyrics", () => - client.lyrics.create({ - data: { - songId: id, - ...input, - }, - omit: { - deletedAt: true, - }, - }) - ); - } - - async getLyricsBySongId(id: SongId, tx?: TxClient) { - const client = tx ?? this.prisma; - - return this.query("db.song.getLyricsBySongId", () => - client.lyrics.findMany({ - where: { songId: id, deletedAt: null }, - omit: { deletedAt: true, songId: true }, - }) - ); - } - - async getLyricById(lyricId: number, tx?: TxClient) { - const client = tx ?? this.prisma; - - return this.query("db.song.getLyricById", () => - client.lyrics.findFirst({ - where: { id: lyricId, deletedAt: null }, - omit: { deletedAt: true, songId: true }, - }) - ); - } - - async updateLyric(lyricId: number, input: SongLyricsUpdateRequestDto, tx?: TxClient) { - const client = tx ?? this.prisma; - - return this.query("db.song.updateLyric", () => - client.lyrics.update({ - where: { id: lyricId }, - data: input, - omit: { deletedAt: true, songId: true }, - }) - ); - } - - async softDeleteLyric(lyricId: number, tx?: TxClient) { - const client = tx ?? this.prisma; - - await this.query("db.song.softDeleteLyric", () => - client.lyrics.update({ - where: { id: lyricId }, - data: { deletedAt: new Date() }, - }) - ); - } -} diff --git a/packages/core/src/modules/catalog/song/service.ts b/packages/core/src/modules/catalog/song/service.ts deleted file mode 100644 index 711a7a3..0000000 --- a/packages/core/src/modules/catalog/song/service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type { OutboxService } from "@cvsa/core/internal"; -import { AppError, type IServiceWithGetDetails } from "@cvsa/core/internal"; -import { prisma } from "@cvsa/db"; -import type { - SongDetailsResponseDto, - SongId, - CreateSongRequestDto, - UpdateSongRequestDto, - SongResponseDto, - SongLyricsCreateRequestDto, - SongLyricsUpdateRequestDto, - SongLyricsResponseDto, - SongLyricsListResponseDto, -} from "./dto"; -import type { ISongRepository } from "./repository.interface"; - -export class SongService implements IServiceWithGetDetails { - constructor( - private readonly repository: ISongRepository, - private readonly outbox: OutboxService - ) {} - - async getDetails(id: SongId) { - const result = await this.repository.getDetailsById(id); - if (result === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - return result; - } - - async create(input: CreateSongRequestDto): Promise { - const { song, entry } = await prisma.$transaction(async (tx) => { - const song = await this.repository.create(input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: song.id, - eventType: "song.created", - }, - tx - ); - return { song, entry }; - }); - - await this.outbox.enqueue(entry); - return song; - } - - async update(id: SongId, input: UpdateSongRequestDto): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - - const { song, entry } = await prisma.$transaction(async (tx) => { - const song = await this.repository.update(id, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: id, - eventType: "song.updated", - }, - tx - ); - return { song, entry }; - }); - - await this.outbox.enqueue(entry); - return song; - } - - async delete(id: SongId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - - const entry = await prisma.$transaction(async (tx) => { - await this.repository.softDelete(id, tx); - return await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: id, - eventType: "song.deleted", - }, - tx - ); - }); - - await this.outbox.enqueue(entry); - } - - async listLyrics(id: SongId): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - return this.repository.getLyricsBySongId(id); - } - - async getLyric(id: SongId, lyricId: number): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - const lyric = await this.repository.getLyricById(lyricId); - if (lyric === null) { - throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); - } - return lyric; - } - - async createLyric( - id: SongId, - input: SongLyricsCreateRequestDto - ): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - - const { lyric, entry } = await prisma.$transaction(async (tx) => { - const lyric = await this.repository.createLyrics(id, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: id, - eventType: "song.lyric_created", - }, - tx - ); - return { lyric, entry }; - }); - - await this.outbox.enqueue(entry); - return lyric; - } - - async updateLyric( - id: SongId, - lyricId: number, - input: SongLyricsUpdateRequestDto - ): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - const lyric = await this.repository.getLyricById(lyricId); - if (lyric === null) { - throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); - } - - const { lyric: updated, entry } = await prisma.$transaction(async (tx) => { - const lyric = await this.repository.updateLyric(lyricId, input, tx); - const entry = await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: id, - eventType: "song.lyric_updated", - }, - tx - ); - return { lyric, entry }; - }); - - await this.outbox.enqueue(entry); - return updated; - } - - async deleteLyric(id: SongId, lyricId: number): Promise { - const existing = await this.repository.getById(id); - if (existing === null) { - throw new AppError("error.song.notfound", "NOT_FOUND", 404); - } - const lyric = await this.repository.getLyricById(lyricId); - if (lyric === null) { - throw new AppError("error.lyric.notfound", "NOT_FOUND", 404); - } - - const entry = await prisma.$transaction(async (tx) => { - await this.repository.softDeleteLyric(lyricId, tx); - return await this.outbox.createEntry( - { - aggregateType: "song", - aggregateId: id, - eventType: "song.lyric_deleted", - }, - tx - ); - }); - - await this.outbox.enqueue(entry); - } -} diff --git a/packages/core/src/modules/index.ts b/packages/core/src/modules/index.ts deleted file mode 100644 index 8c4f84d..0000000 --- a/packages/core/src/modules/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./catalog"; -export * from "./auth"; -export * from "./outbox"; diff --git a/packages/core/src/modules/outbox/container.ts b/packages/core/src/modules/outbox/container.ts deleted file mode 100644 index 90f4d62..0000000 --- a/packages/core/src/modules/outbox/container.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { prisma } from "@cvsa/db"; -import { OutboxRepository } from "./repository"; -import { OutboxService } from "./service"; - -export const outboxRepository = new OutboxRepository(prisma); -export const outboxService = new OutboxService(outboxRepository); diff --git a/packages/core/src/modules/outbox/dto.ts b/packages/core/src/modules/outbox/dto.ts deleted file mode 100644 index 95438b0..0000000 --- a/packages/core/src/modules/outbox/dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { z } from "zod"; - -export const CreateOutboxEntrySchema = z.object({ - aggregateType: z.string(), - aggregateId: z.number().int(), - eventType: z.string(), - payload: z.record(z.string(), z.unknown()).optional(), -}); - -export const PendingOutboxQuerySchema = z.object({ - limit: z.number().int().positive().default(100), -}); - -export type CreateOutboxEntryDto = { - aggregateType: string; - aggregateId: number; - eventType: string; - payload?: Record; -}; - -export type OutboxEntryDto = { - id: number; - aggregateType: string; - aggregateId: number; - eventType: string; - payload: unknown; - status: "PENDING" | "PROCESSING" | "PROCESSED" | "FAILED"; - retryCount: number; - lastError: string | null; - nextRetryAt: string | null; - createdAt: string; - processedAt: string | null; -}; - -export type PendingOutboxQueryDto = z.infer; diff --git a/packages/core/src/modules/outbox/index.ts b/packages/core/src/modules/outbox/index.ts deleted file mode 100644 index 132cd4a..0000000 --- a/packages/core/src/modules/outbox/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dto"; -export * from "./repository"; -export * from "./repository.interface"; -export * from "./service"; -export * from "./container"; diff --git a/packages/core/src/modules/outbox/repository.interface.ts b/packages/core/src/modules/outbox/repository.interface.ts deleted file mode 100644 index 2317b39..0000000 --- a/packages/core/src/modules/outbox/repository.interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { TxClient } from "@cvsa/db"; -import type { CreateOutboxEntryDto, OutboxEntryDto, PendingOutboxQueryDto } from "./dto"; - -export abstract class IOutboxRepository { - abstract create(input: CreateOutboxEntryDto, tx?: TxClient): Promise; - abstract findPending(query: PendingOutboxQueryDto): Promise; - abstract markProcessing(id: number, tx?: TxClient): Promise; - abstract markProcessed(id: number, tx?: TxClient): Promise; - abstract markFailed( - id: number, - lastError: string, - nextRetryAt: Date, - tx?: TxClient - ): Promise; -} diff --git a/packages/core/src/modules/outbox/repository.ts b/packages/core/src/modules/outbox/repository.ts deleted file mode 100644 index aa5d46e..0000000 --- a/packages/core/src/modules/outbox/repository.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { PrismaClient } from "@cvsa/db"; -import { transformPrismaResult, type TxClient } from "@cvsa/db"; -import type { CreateOutboxEntryDto, PendingOutboxQueryDto } from "./dto"; -import type { IOutboxRepository } from "./repository.interface"; - -export class OutboxRepository implements IOutboxRepository { - constructor(private readonly prisma: PrismaClient) {} - - async create(input: CreateOutboxEntryDto, tx?: TxClient) { - const client = tx ?? this.prisma; - return transformPrismaResult( - await client.outbox.create({ - data: { - aggregateType: input.aggregateType, - aggregateId: input.aggregateId, - eventType: input.eventType, - payload: input.payload ?? null, - }, - }) - ); - } - - async findPending(query: PendingOutboxQueryDto) { - const client = this.prisma; - return transformPrismaResult( - await client.outbox.findMany({ - where: { - status: "PENDING", - OR: [{ nextRetryAt: { lte: new Date() } }, { nextRetryAt: null }], - }, - orderBy: { createdAt: "asc" }, - take: query.limit, - }) - ); - } - - async markProcessing(id: number, tx?: TxClient) { - const client = tx ?? this.prisma; - return transformPrismaResult( - await client.outbox.update({ - where: { id }, - data: { status: "PROCESSING" }, - }) - ); - } - - async markProcessed(id: number, tx?: TxClient) { - const client = tx ?? this.prisma; - return transformPrismaResult( - await client.outbox.update({ - where: { id }, - data: { status: "PROCESSED", processedAt: new Date() }, - }) - ); - } - - async markFailed(id: number, lastError: string, nextRetryAt: Date, tx?: TxClient) { - const client = tx ?? this.prisma; - return transformPrismaResult( - await client.outbox.update({ - where: { id }, - data: { - status: "FAILED", - lastError, - nextRetryAt, - retryCount: { increment: 1 }, - }, - }) - ); - } -} diff --git a/packages/core/src/modules/outbox/service.ts b/packages/core/src/modules/outbox/service.ts deleted file mode 100644 index 14a30de..0000000 --- a/packages/core/src/modules/outbox/service.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Queue } from "bullmq"; -import type { IOutboxRepository } from "./repository.interface"; -import type { CreateOutboxEntryDto, OutboxEntryDto, PendingOutboxQueryDto } from "./dto"; -import { Prisma, type TxClient } from "@cvsa/db"; -import { outboxQueue } from "../../outbox/queue"; -import { appLogger } from "@cvsa/logger"; -import { traceTask } from "@cvsa/observability"; - -const MAX_RETRIES = 5; - -export class OutboxService { - private readonly queue: Queue; - - constructor( - private readonly repository: IOutboxRepository, - queue?: Queue - ) { - this.queue = queue ?? outboxQueue; - } - - async createEntry(input: CreateOutboxEntryDto, tx: TxClient): Promise { - return traceTask("outbox.createEntry", () => this.repository.create(input, tx)); - } - - async enqueue(entry: OutboxEntryDto): Promise { - await traceTask("outbox.enqueue", async () => { - await this.queue.add(`outbox-${entry.aggregateType}-${entry.aggregateId}`, entry, { - jobId: `outbox-${entry.id}`, - attempts: MAX_RETRIES, - backoff: { type: "exponential", delay: 1000 }, - }); - }); - } - - async processEntry( - id: number, - processor: (aggregateType: string, aggregateId: number, eventType: string) => Promise - ): Promise { - try { - const entry = await this.repository.markProcessing(id); - try { - await processor(entry.aggregateType, entry.aggregateId, entry.eventType); - await this.repository.markProcessed(id); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - appLogger.warn(`Outbox entry ${id} processing failed: ${errorMessage}`); - if (entry.retryCount >= MAX_RETRIES - 1) { - await this.repository.markFailed( - id, - errorMessage, - new Date(Date.now() + 3600_000) - ); - } else { - throw error; - } - } - } catch (error) { - // Handle case where outbox entry no longer exists (already processed or cleaned up) - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { - appLogger.debug(`Outbox entry ${id} not found, skipping processing`); - return; - } - throw error; - } - } - - async recoverStaleEntries(limit: number = 100): Promise { - const query: PendingOutboxQueryDto = { limit }; - const entries = await this.repository.findPending(query); - for (const entry of entries) { - await this.queue.add(`outbox-recover-${entry.id}`, entry, { - jobId: `outbox-recover-${entry.id}`, - attempts: MAX_RETRIES, - backoff: { type: "exponential", delay: 1000 }, - }); - } - } -} diff --git a/packages/core/src/outbox/index.ts b/packages/core/src/outbox/index.ts deleted file mode 100644 index 1336656..0000000 --- a/packages/core/src/outbox/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./queue"; -export * from "./processor"; diff --git a/packages/core/src/outbox/processor.ts b/packages/core/src/outbox/processor.ts deleted file mode 100644 index e2f772f..0000000 --- a/packages/core/src/outbox/processor.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Job } from "bullmq"; -import type { OutboxEntryDto, OutboxService } from "@cvsa/core/internal"; -import { - songSearchService, - artistSearchService, - singerSearchService, - artistRoleSearchService, -} from "@cvsa/core/internal"; -import { outboxService } from "../modules/outbox/container"; -import { appLogger } from "@cvsa/logger"; - -export interface OutboxProcessorDeps { - outboxService: OutboxService; - searchServices: Record }>; -} - -export function createOutboxProcessor( - deps: OutboxProcessorDeps -): (job: Job) => Promise { - return async function processOutboxEntry(job: Job): Promise { - const entry = job.data; - - await deps.outboxService.processEntry( - entry.id, - async (aggregateType, aggregateId, eventType) => { - appLogger.debug( - `processing outbox entry with ID ${aggregateId}, type ${aggregateType}`, - { - aggregateId, - aggregateType, - eventType, - } - ); - - const searchService = deps.searchServices[aggregateType]; - if (!searchService) { - appLogger.warn(`Unknown aggregate type: ${aggregateType}`); - throw new Error(`Unknown aggregate type: ${aggregateType}`); - } - - await searchService.sync(aggregateId); - } - ); - }; -} - -export const processOutboxEntry = createOutboxProcessor({ - outboxService, - searchServices: { - song: songSearchService, - artist: artistSearchService, - singer: singerSearchService, - artistRole: artistRoleSearchService, - }, -}); diff --git a/packages/core/src/outbox/queue.ts b/packages/core/src/outbox/queue.ts deleted file mode 100644 index 9596e5a..0000000 --- a/packages/core/src/outbox/queue.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Queue, Worker, type Job } from "bullmq"; -import IORedis from "ioredis"; -import { env } from "@cvsa/env"; -import { appLogger } from "@cvsa/logger"; -import type { OutboxEntryDto } from "@cvsa/core/internal"; - -const QUEUE_NAME = "outbox"; - -let connection: IORedis | undefined; - -function getConnection(): IORedis { - if (!connection) { - appLogger.debug(`Connecting to redis at ${env.REDIS_URL}`); - connection = new IORedis(env.REDIS_URL, { - maxRetriesPerRequest: null, - }); - } - return connection; -} - -export const outboxQueue = new Queue(QUEUE_NAME, { - connection: getConnection(), - defaultJobOptions: { - attempts: 5, - backoff: { - type: "exponential", - delay: 1000, - }, - removeOnComplete: true, - removeOnFail: { - age: 24 * 3600, - count: 1000, - }, - }, -}); - -export function createOutboxWorker( - processor: (job: Job) => Promise -): Worker { - const worker = new Worker(QUEUE_NAME, processor, { - connection: getConnection(), - concurrency: 5, - }); - - worker.on("completed", (job) => { - appLogger.info(`Outbox job ${job.id} completed`); - }); - - worker.on("failed", (job, err) => { - appLogger.error(`Outbox job ${job?.id} failed: ${err.message}`); - }); - - return worker; -} - -export async function closeOutboxInfrastructure(): Promise { - await outboxQueue.close(); - if (connection) { - await connection.quit(); - connection = undefined; - } -} diff --git a/packages/core/src/search/catalog/artist.ts b/packages/core/src/search/catalog/artist.ts deleted file mode 100644 index d2e90d1..0000000 --- a/packages/core/src/search/catalog/artist.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ISearchService } from "../interface"; -import type { ArtistDetailsResponseDto } from "../../modules"; - -export interface ArtistSearchIndex { - id: number; - name?: string; - description?: string; - aliases?: string[]; - _vectors?: { - "potion-multilingual-128M": number[] | null; - }; -} - -export class ArtistSearchService extends ISearchService< - ArtistDetailsResponseDto, - ArtistSearchIndex -> { - protected readonly entityType = "artist"; - - protected async getDocument( - artist: ArtistDetailsResponseDto, - language: string - ): Promise { - const vectors = await this.embeddingManager.embeddings.post({ - texts: [ - `Name: ${this.getName(artist, language) ?? ""} -Description: ${this.getDescription(artist, language) ?? ""} -Name Aliases: ${artist.aliases.join(", ")} -`, - ], - }); - return { - id: artist.id, - name: this.getName(artist, language) ?? undefined, - aliases: artist.aliases, - description: this.getDescription(artist, language) ?? undefined, - _vectors: this.buildVectors(vectors.data?.embeddings[0]), - }; - } -} diff --git a/packages/core/src/search/catalog/artistRole.ts b/packages/core/src/search/catalog/artistRole.ts deleted file mode 100644 index c62e20e..0000000 --- a/packages/core/src/search/catalog/artistRole.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ISearchService } from "../interface"; -import type { ArtistRoleDetailsResponseDto } from "../../modules"; - -export interface ArtistRoleSearchIndex { - id: number; - name: string; - localizedNames?: Record | null; - _vectors?: { - "potion-multilingual-128M": number[] | null; - }; -} - -export class ArtistRoleSearchService extends ISearchService< - ArtistRoleDetailsResponseDto, - ArtistRoleSearchIndex -> { - protected readonly entityType = "artistRole"; - - protected async getDocument( - role: ArtistRoleDetailsResponseDto, - language: string - ): Promise { - const vectors = await this.embeddingManager.embeddings.post({ - texts: [`Name: ${this.getName(role, language) ?? ""}`], - }); - return { - id: role.id, - name: this.getName(role, language) ?? "", - localizedNames: role.localizedNames, - _vectors: this.buildVectors(vectors.data?.embeddings[0]), - }; - } -} diff --git a/packages/core/src/search/catalog/index.ts b/packages/core/src/search/catalog/index.ts deleted file mode 100644 index 79d74fc..0000000 --- a/packages/core/src/search/catalog/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./song"; -export * from "./artist"; -export * from "./artistRole"; -export * from "./singer"; diff --git a/packages/core/src/search/catalog/singer.ts b/packages/core/src/search/catalog/singer.ts deleted file mode 100644 index 79bede8..0000000 --- a/packages/core/src/search/catalog/singer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ISearchService } from "../interface"; -import type { SingerDetailsResponseDto } from "../../modules"; - -export interface SingerSearchIndex { - id: number; - name?: string; - description?: string; - language?: string; - _vectors?: { - "potion-multilingual-128M": number[] | null; - }; -} - -export class SingerSearchService extends ISearchService< - SingerDetailsResponseDto, - SingerSearchIndex -> { - protected readonly entityType = "singer"; - - protected async getDocument( - singer: SingerDetailsResponseDto, - language: string - ): Promise { - const vectors = await this.embeddingManager.embeddings.post({ - texts: [ - `Name: ${this.getName(singer, language) ?? ""} -Description: ${this.getDescription(singer, language) ?? ""} -`, - ], - }); - return { - id: singer.id, - name: this.getName(singer, language) ?? undefined, - language: singer.language, - description: this.getDescription(singer, language) ?? undefined, - _vectors: this.buildVectors(vectors.data?.embeddings[0]), - }; - } -} diff --git a/packages/core/src/search/catalog/song.ts b/packages/core/src/search/catalog/song.ts deleted file mode 100644 index c405148..0000000 --- a/packages/core/src/search/catalog/song.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { SongType } from "@cvsa/db"; -import { ISearchService } from "../interface"; -import type { SongDetailsResponseDto } from "../../modules"; - -export interface SongSearchIndex { - id: number; - name?: string; - lyrics?: string; - description?: string; - singers?: string[]; - artists?: string[]; - bilibiliAid?: number; - bilibiliBvid?: string; - type?: SongType; - tags?: string[]; - engine?: string[]; - publishedAt?: number; - bilibiliViews?: number; - _vectors?: { - "potion-multilingual-128M": number[] | null; - }; -} - -export class SongSearchService extends ISearchService { - protected readonly entityType = "song"; - - protected async getDocument( - song: SongDetailsResponseDto, - language: string - ): Promise { - const getLyrics = () => { - return song.lyrics.find((item) => item.language === language)?.plainText; - }; - const getLocalizedRelatives = (attr: "singers" | "artists") => { - const data = song[attr].map((item) => { - if (item.language === language) { - return item.name; - } - return item.localizedNames?.[language]; - }); - return data.filter(Boolean) as string[]; - }; - const getSingers = () => { - return getLocalizedRelatives("singers"); - }; - const getArtists = () => { - return getLocalizedRelatives("artists"); - }; - - const vectors = await this.embeddingManager.embeddings.post({ - texts: [ - `Name: ${this.getName(song, language) ?? ""} -Lyrics: ${getLyrics() ?? ""} -Description: ${this.getDescription(song, language) ?? ""} -Singers: ${getSingers().join(", ")} -Artists: ${getArtists().join(", ")} -`, - ], - }); - return { - id: song.id, - name: this.getName(song, language) ?? undefined, - lyrics: getLyrics() ?? undefined, - description: this.getDescription(song, language) ?? undefined, - singers: getSingers(), - artists: getArtists(), - bilibiliAid: song.bilibiliAid ?? undefined, - bilibiliBvid: song.bilibiliBvid ?? undefined, - type: song.type ?? undefined, - engine: song.singers - .map((item) => item.engine ?? undefined) - .filter(Boolean) as string[], - publishedAt: song.publishedAt ? new Date(song.publishedAt).getTime() : undefined, - _vectors: this.buildVectors(vectors.data?.embeddings[0]), - }; - } -} diff --git a/packages/core/src/search/config.ts b/packages/core/src/search/config.ts deleted file mode 100644 index f662ea3..0000000 --- a/packages/core/src/search/config.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Settings } from "meilisearch"; -// import { env } from "@cvsa/core/common"; - -export const INDEX_SETTINGS: Record = { - song: { - searchableAttributes: [ - "name", - "lyrics", - "description", - "singers", - "artists", - "bilibiliAid", - "bilibiliBvid", - ], - filterableAttributes: ["type", "tags", "singers", "engine"], - sortableAttributes: ["publishedAt", "bilibiliViews"], - rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], - embedders: { - "potion-multilingual-128M": { - source: "userProvided", - dimensions: 256, - }, - // TODO: Remove this - // "qwen3-embedding-8b-openrouter": { - // source: "rest", - // url: "https://openrouter.ai/api/v1/embeddings", - // request: { - // model: "qwen/qwen3-embedding-8b", - // input: ["{{text}}", "{{..}}"], - // encoding_format: "float", - // }, - // response: { - // data: [{ embedding: "{{embedding}}" }, "{{..}}"], - // }, - // apiKey: env.OPENROUTER_KEY, - // dimensions: 4096, - // documentTemplate: `Instruct: You are providing search results for songs in the database. - // Query: This song has the following properties. - // Name: {{doc.name}}, - - // Singers: {% for singer in doc.singers %} - // {{ singer }} - // {% endfor %} - // Artists: {% for artist in doc.artists %} - // {{ artist }} - // {% endfor %}`, - // }, - }, - }, - artist: { - searchableAttributes: ["name", "description", "aliases"], - rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], - embedders: { - "potion-multilingual-128M": { - source: "userProvided", - dimensions: 256, - }, - }, - }, - singer: { - searchableAttributes: ["name", "description", "language"], - rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], - embedders: { - "potion-multilingual-128M": { - source: "userProvided", - dimensions: 256, - }, - }, - }, - artistRole: { - searchableAttributes: ["name"], - rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], - embedders: { - "potion-multilingual-128M": { - source: "userProvided", - dimensions: 256, - }, - }, - }, -}; diff --git a/packages/core/src/search/container.ts b/packages/core/src/search/container.ts deleted file mode 100644 index 5727a55..0000000 --- a/packages/core/src/search/container.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SearchManager } from "./manager"; - -export const searchManager = await SearchManager.create(); diff --git a/packages/core/src/search/index.ts b/packages/core/src/search/index.ts deleted file mode 100644 index dd28cf7..0000000 --- a/packages/core/src/search/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./catalog"; -export * from "./container"; -export * from "./interface"; diff --git a/packages/core/src/search/interface.ts b/packages/core/src/search/interface.ts deleted file mode 100644 index 8aeb662..0000000 --- a/packages/core/src/search/interface.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { IRepositoryWithGetDetails } from "@cvsa/core/internal"; -import type { SearchManager } from "./manager"; -import type { SearchResponse } from "meilisearch"; -import type { EmbeddingAppApi } from "@cvsa/embedding"; -import { unique, keys } from "remeda"; -import { appLogger } from "@cvsa/logger"; - -export interface LocalizableEntity { - id: number; - name?: string | null; - language: string; - description?: string | null; - localizedNames?: Record | null; - localizedDescriptions?: Record | null; -} - -export abstract class ISearchService { - protected abstract readonly entityType: string; - - constructor( - protected readonly repository: IRepositoryWithGetDetails, - protected readonly searchManager: SearchManager | undefined, - protected readonly embeddingManager: EmbeddingAppApi - ) {} - - protected abstract getDocument(entity: T, language: string): Promise; - - protected getName(entity: T, language: string): string | undefined { - if (language === entity.language) return entity.name ?? undefined; - return entity.localizedNames?.[language]; - } - - protected getDescription(entity: T, language: string): string | undefined { - if (language === entity.language) return entity.description ?? undefined; - return entity.localizedDescriptions?.[language]; - } - - protected collectLanguages(entity: T): string[] { - return unique([ - ...keys(entity.localizedNames ?? {}), - ...keys(entity.localizedDescriptions ?? {}), - entity.language, - ]); - } - - protected buildVectors(embedding: number[] | undefined): { - "potion-multilingual-128M": number[] | null; - } { - return embedding - ? { "potion-multilingual-128M": embedding } - : { "potion-multilingual-128M": null }; - } - - public async sync(id: number): Promise { - if (!this.searchManager) { - appLogger.warn("Search service not available"); - return; - } - const entity = await this.repository.getDetailsById(id); - - if (!entity) { - const indexes = await this.searchManager.getLocalizedIndexesOfEntity(this.entityType); - for (const indexName of indexes) { - const adminIndex = await this.searchManager.getAdminIndex(indexName); - const task = await adminIndex.deleteDocument(id); - await this.searchManager.waitForTask(task.taskUid); - } - return; - } - - const languages = this.collectLanguages(entity); - for (const language of languages) { - const indexUid = `${this.entityType}_${language}`; - const index = await this.searchManager.getAdminIndex(indexUid); - const document = await this.getDocument(entity, language); - const task = await index.addDocuments([document], { - primaryKey: "id", - }); - await this.searchManager.waitForTask(task.taskUid); - } - } - - public async search(query: string, language: string = "zh"): Promise { - if (!this.searchManager) { - throw new Error("Search or embedding service not available"); - } - - const index = await this.searchManager.getSearchIndex(`${this.entityType}_${language}`); - const embeddingResponse = await this.embeddingManager.embeddings.post({ - texts: [query], - }); - const embeddingAvailable = (embeddingResponse?.data?.embeddings[0]?.length ?? 0) > 0; - - return index.search(query, { - vector: embeddingResponse?.data?.embeddings[0], - hybrid: embeddingAvailable - ? { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - } - : undefined, - showRankingScore: true, - }); - } -} diff --git a/packages/core/src/search/manager.ts b/packages/core/src/search/manager.ts deleted file mode 100644 index b252098..0000000 --- a/packages/core/src/search/manager.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { MeiliSearch, type Settings, type RecordAny, type IndexOptions } from "meilisearch"; -import { env } from "@cvsa/env"; -import { INDEX_SETTINGS } from "./config"; -import { deepEqualUnordered } from "../utils"; -import { appLogger } from "@cvsa/logger"; - -export class SearchManager { - private constructor( - private client?: MeiliSearch, - private adminClient?: MeiliSearch - ) {} - - /** - * Creates a new SearchManager instance with separate search and admin clients. - * Initializes MeiliSearch clients using API keys retrieved from the master client. - * The search client has only search permissions, while the admin client has full permissions. - * Returns a SearchManager instance even if initialization fails - methods will retry initialization on each call. - * @returns A promise that resolves to a new SearchManager instance - */ - public static async create(customClient?: MeiliSearch, customAdminClient?: MeiliSearch) { - if (customClient && customAdminClient) { - return new SearchManager(customClient, customAdminClient); - } - - const manager = new SearchManager(undefined, undefined); - await manager.initializeClients(); - return manager; - } - - /** - * Attempts to initialize the MeiliSearch clients. - * Safe to call multiple times - will retry if previous initialization failed. - */ - private async initializeClients(): Promise { - if (this.client !== undefined && this.adminClient !== undefined) { - return; - } - - try { - const masterClient = new MeiliSearch({ - host: env.MEILI_API_URL, - apiKey: env.MEILI_MASTER_KEY, - }); - const adminKey = await SearchManager.getAdminKey(masterClient); - const searchKey = await SearchManager.getSearchKey(masterClient); - this.client = new MeiliSearch({ - host: env.MEILI_API_URL, - apiKey: searchKey, - }); - this.adminClient = new MeiliSearch({ - host: env.MEILI_API_URL, - apiKey: adminKey, - }); - await this.syncAllSettings(); - } catch (e) { - appLogger.warn("Cannot initialize SearchManager clients."); - appLogger.error(Bun.inspect(e)); - throw e; - } - } - - private ensureInitialized(): void { - if (this.client === undefined || this.adminClient === undefined) { - throw new Error("SearchManager not initialized"); - } - } - - private static async getOrCreateKey( - client: MeiliSearch, - params: { actions: string[]; indexes: string[]; name: string } - ): Promise { - const { results: keys } = await client.getKeys({ limit: 1000 }); - - const existingKey = keys.find( - (key) => - deepEqualUnordered(key.actions, params.actions) && - deepEqualUnordered(key.indexes, params.indexes) - ); - - if (existingKey) { - return existingKey.key; - } - - const newKey = await client.createKey({ - ...params, - expiresAt: null, - }); - - return newKey.key; - } - - private static async getAdminKey(client: MeiliSearch): Promise { - return SearchManager.getOrCreateKey(client, { - actions: ["*"], - indexes: ["*"], - name: "Admin API Key", - }); - } - - private static async getSearchKey(client: MeiliSearch): Promise { - return SearchManager.getOrCreateKey(client, { - actions: ["search"], - indexes: ["*"], - name: "Search API Key", - }); - } - - /** - * Waits for a MeiliSearch task to complete. - * @param taskUid - The unique identifier of the task to wait for - * @returns A promise that resolves to the completed task - */ - public async waitForTask(taskUid: number) { - await this.initializeClients(); - this.ensureInitialized(); - const adminClient = this.adminClient as MeiliSearch; - return await adminClient.tasks.waitForTask(taskUid, { - timeout: 10000, - interval: 100, - }); - } - - /** - * Retrieves a specific task by its UID. - * @param taskUid - The unique identifier of the task - * @returns A promise that resolves to the task details - */ - public async getTask(taskUid: number) { - await this.initializeClients(); - this.ensureInitialized(); - const adminClient = this.adminClient as MeiliSearch; - return await adminClient.tasks.getTask(taskUid); - } - - /** - * Returns an admin-index for the specified index name. - * Admin index has full permissions including write and delete operations. - * @param indexName - The name of the index - * @returns The admin MeiliSearch index instance - */ - public async getAdminIndex(indexName: string) { - await this.initializeClients(); - this.ensureInitialized(); - const adminClient = this.adminClient as MeiliSearch; - return adminClient.index(indexName); - } - - /** - * Returns a search-index for the specified index name. - * Search index has read-only permissions for search operations only. - * @param indexName - The name of the index - * @returns The search-only MeiliSearch index instance - */ - public async getSearchIndex(indexName: string) { - await this.initializeClients(); - this.ensureInitialized(); - const client = this.client as MeiliSearch; - return client.index(indexName); - } - - /** - * Lists all indexes in the MeiliSearch instance. - * @returns A promise that resolves to the list of all indexes - */ - public async listIndexes() { - await this.initializeClients(); - this.ensureInitialized(); - const adminClient = this.adminClient as MeiliSearch; - return adminClient.getIndexes(); - } - - /** - * Gets all localized indexes for a given entity name. - * - * Expects index UIDs in the format `{entityName}_{locale}` (e.g., `artist_zh-CN`). - * @param name - The entity name to filter indexes by - * @returns A promise that resolves to an array of matching index UIDs - */ - public async getLocalizedIndexesOfEntity(name: string) { - await this.initializeClients(); - this.ensureInitialized(); - const adminClient = this.adminClient as MeiliSearch; - const { results: indexes } = await adminClient.getIndexes({ - limit: 1000, - }); - const results: string[] = []; - for (const index of indexes) { - const [indexName, _] = index.uid.split("_"); - if (indexName === name) results.push(index.uid); - } - return results; - } - - /** - * Synchronizes settings for all indexes with the configured INDEX_SETTINGS. - * Only updates settings that differ from current values and triggers reindex - * for settings that require it (non-displayedAttributes and non-rankingRules). - * Skips indexes without a corresponding entry in INDEX_SETTINGS. - */ - public async syncAllSettings() { - if (this.adminClient === undefined) { - return; - } - - const { results: indexes } = await this.listIndexes(); - for (const index of indexes) { - const uid = index.uid; - const [name, lang] = uid.split("_"); - if (lang === undefined) continue; - if (!(name in INDEX_SETTINGS)) continue; - const settings = INDEX_SETTINGS[name as keyof typeof INDEX_SETTINGS]; - const currentSettings = await index.getSettings(); - for (const [key, value] of Object.entries(settings)) { - if (deepEqualUnordered(value, currentSettings[key as keyof Settings])) continue; - // These settings do not trigger a full reindex - if (["displayedAttributes", "rankingRules"].includes(key)) continue; - const resetTask = await index.resetSettings(); - await this.waitForTask(resetTask.taskUid); - const updateTask = await index.updateSettings(settings); - await this.waitForTask(updateTask.taskUid); - break; - } - } - } - - /** - * Deletes all search indexes in the MeiliSearch instance. - * This is a destructive operation that removes all indexes and their data. - * Safely no-ops if the admin client is not initialized. - */ - public async clearAllIndex() { - if (this.adminClient === undefined) { - return; - } - - const { results: indexes } = await this.listIndexes(); - for (const index of indexes) { - await this.adminClient.deleteIndexIfExists(index.uid); - appLogger.info(`Deleted search index ${index.uid}`); - } - } - - /** - * Creates a new search index with the given UID and optional configuration. - * Safely no-ops if the admin client is not initialized. - * @param uid - The unique identifier for the index - * @param options - Optional index configuration (primary key, etc.) - * @returns A promise that resolves to the created index task, or undefined if not initialized - */ - public async createIndex(uid: string, options?: IndexOptions) { - if (this.adminClient === undefined) { - return; - } - return this.adminClient.createIndex(uid, options); - } -} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts deleted file mode 100644 index 3dfbbec..0000000 --- a/packages/core/src/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./repository"; -export * from "./service"; diff --git a/packages/core/src/types/repository.ts b/packages/core/src/types/repository.ts deleted file mode 100644 index ac40ce5..0000000 --- a/packages/core/src/types/repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { TxClient } from "@cvsa/db"; - -export interface IRepositoryWithGetDetails { - getDetailsById(id: number | string, tx?: TxClient): Promise; -} diff --git a/packages/core/src/types/service.ts b/packages/core/src/types/service.ts deleted file mode 100644 index 9741901..0000000 --- a/packages/core/src/types/service.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IServiceWithGetDetails { - getDetails(id: number | string): Promise; -} diff --git a/packages/core/src/utils/BaseRepository.ts b/packages/core/src/utils/BaseRepository.ts deleted file mode 100644 index fe9ef54..0000000 --- a/packages/core/src/utils/BaseRepository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { transformPrismaResult, type Serialized } from "@cvsa/db"; -import { traceTask } from "@cvsa/observability"; - -export abstract class BaseRepository { - protected async query(name: string, fn: () => Promise): Promise> { - return traceTask(name, async () => transformPrismaResult(await fn())); - } -} diff --git a/packages/core/src/utils/deepEqualUnordered.ts b/packages/core/src/utils/deepEqualUnordered.ts deleted file mode 100644 index 3a292e1..0000000 --- a/packages/core/src/utils/deepEqualUnordered.ts +++ /dev/null @@ -1,37 +0,0 @@ -type PlainObject = Record; - -const isObject = (obj: unknown): obj is PlainObject => { - return obj !== null && typeof obj === "object" && !Array.isArray(obj); -}; - -export const deepEqualUnordered = (a: unknown, b: unknown): boolean => { - if (a === b) return true; - - if (Array.isArray(a) && Array.isArray(b)) { - if (a.length !== b.length) return false; - - const bCopy = [...b]; - for (const itemA of a) { - const indexB = bCopy.findIndex((itemB) => deepEqualUnordered(itemA, itemB)); - if (indexB === -1) return false; - bCopy.splice(indexB, 1); - } - return true; - } - - if (isObject(a) && isObject(b)) { - const keysA = Object.keys(a); - const keysB = Object.keys(b); - - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - // biome-ignore lint/suspicious/noPrototypeBuiltins: utility function - if (!Object.prototype.hasOwnProperty.call(b, key)) return false; - if (!deepEqualUnordered(a[key], b[key])) return false; - } - return true; - } - - return false; -}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts deleted file mode 100644 index c03d97a..0000000 --- a/packages/core/src/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./randomId"; -export * from "./deepEqualUnordered"; -export * from "./BaseRepository"; diff --git a/packages/core/src/utils/randomId.ts b/packages/core/src/utils/randomId.ts deleted file mode 100644 index ed76be3..0000000 --- a/packages/core/src/utils/randomId.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { customAlphabet } from "nanoid"; -const alphabet = "123456789ABCDEFGHJKMNPQRSTWXYZ"; -const fn = customAlphabet(alphabet); -export const getRandomId = (length: number) => fn(length); diff --git a/packages/core/tests/integration/ArtistRepository.test.ts b/packages/core/tests/integration/ArtistRepository.test.ts deleted file mode 100644 index 5337078..0000000 --- a/packages/core/tests/integration/ArtistRepository.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - artistRepository, - type CreateArtistRequestDto, - type UpdateArtistRequestDto, -} from "@cvsa/core"; - -const repository = artistRepository; - -describe("ArtistRepository Integration Tests", () => { - describe("create", () => { - test("should create an artist with all fields", async () => { - const input: CreateArtistRequestDto = { - name: "月华P", - localizedNames: { ja: "月華P", en: "Gekka P" }, - language: "zh", - aliases: ["月光P", "Moonlight"], - description: "A famous composer in the Chinese vocal synth community", - localizedDescriptions: { - en: "A famous composer", - ja: "有名な作曲家", - }, - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("月华P"); - expect(result.localizedNames).toEqual({ ja: "月華P", en: "Gekka P" }); - expect(result.language).toBe("zh"); - expect(result.aliases).toEqual(["月光P", "Moonlight"]); - expect(result.description).toBe( - "A famous composer in the Chinese vocal synth community" - ); - expect(result.localizedDescriptions).toEqual({ - en: "A famous composer", - ja: "有名な作曲家", - }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should create an artist with minimal fields", async () => { - const input: CreateArtistRequestDto = { - name: "无名P", - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("无名P"); - expect(result.localizedNames).toBeNull(); - // language has default value "zh" in database - expect(result.language).toBe("zh"); - // aliases is String[] with default empty array - expect(result.aliases).toEqual([]); - expect(result.description).toBeNull(); - expect(result.localizedDescriptions).toBeNull(); - }); - }); - - describe("getById", () => { - test("should return artist when exists", async () => { - const created = await repository.create({ name: "赤羽P" }); - const result = await repository.getById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("赤羽P"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when artist does not exist", async () => { - const result = await repository.getById(999999); - expect(result).toBeNull(); - }); - }); - - describe("getDetailsById", () => { - test("should return artist details when exists", async () => { - const created = await repository.create({ - name: "星尘P", - description: "Composer of 星尘 series", - }); - const result = await repository.getDetailsById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("星尘P"); - expect(result?.description).toBe("Composer of 星尘 series"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when artist does not exist", async () => { - const result = await repository.getDetailsById(999999); - expect(result).toBeNull(); - }); - }); - - describe("update", () => { - test("should update all fields", async () => { - const created = await repository.create({ - name: "Old Name", - language: "zh", - aliases: ["old"], - }); - - const input: UpdateArtistRequestDto = { - name: "New Name", - language: "en", - aliases: ["new", "alias"], - description: "Updated description", - }; - - const result = await repository.update(created.id, input); - - expect(result.name).toBe("New Name"); - expect(result.language).toBe("en"); - expect(result.aliases).toEqual(["new", "alias"]); - expect(result.description).toBe("Updated description"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should update only specified fields", async () => { - const created = await repository.create({ - name: "Original Name", - language: "zh", - description: "Original description", - }); - - const result = await repository.update(created.id, { name: "Updated Name" }); - - expect(result.name).toBe("Updated Name"); - expect(result.language).toBe("zh"); - expect(result.description).toBe("Original description"); - }); - }); - - describe("softDelete", () => { - test("should soft delete an artist", async () => { - const created = await repository.create({ name: "Artist to Delete" }); - - await repository.softDelete(created.id); - - const result = await repository.getById(created.id); - expect(result).toBeNull(); - }); - }); -}); diff --git a/packages/core/tests/integration/ArtistRoleRepository.test.ts b/packages/core/tests/integration/ArtistRoleRepository.test.ts deleted file mode 100644 index 8b68954..0000000 --- a/packages/core/tests/integration/ArtistRoleRepository.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - artistRoleRepository, - type CreateArtistRoleRequestDto, - type UpdateArtistRoleRequestDto, -} from "@cvsa/core"; - -const repository = artistRoleRepository; - -describe("ArtistRoleRepository Integration Tests", () => { - describe("create", () => { - test("should create an artist role with all fields", async () => { - const input: CreateArtistRoleRequestDto = { - name: "Producer", - localizedNames: { zh: "制作人", ja: "プロデューサー" }, - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("Producer"); - expect(result.localizedNames).toEqual({ zh: "制作人", ja: "プロデューサー" }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should create an artist role with minimal fields", async () => { - const input: CreateArtistRoleRequestDto = { - name: "Composer", - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("Composer"); - expect(result.localizedNames).toBeNull(); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - }); - - describe("getById", () => { - test("should return role when exists", async () => { - const created = await repository.create({ name: "Illustrator" }); - const result = await repository.getById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("Illustrator"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when role does not exist", async () => { - const result = await repository.getById(999999); - expect(result).toBeNull(); - }); - }); - - describe("getDetailsById", () => { - test("should return role details when exists", async () => { - const created = await repository.create({ - name: "Lyricist", - localizedNames: { zh: "作词人" }, - }); - const result = await repository.getDetailsById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("Lyricist"); - expect(result?.localizedNames).toEqual({ zh: "作词人" }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when role does not exist", async () => { - const result = await repository.getDetailsById(999999); - expect(result).toBeNull(); - }); - }); - - describe("update", () => { - test("should update all fields", async () => { - const created = await repository.create({ - name: "Old Role", - localizedNames: { zh: "旧角色" }, - }); - - const input: UpdateArtistRoleRequestDto = { - name: "New Role", - localizedNames: { zh: "新角色", en: "New Role" }, - }; - - const result = await repository.update(created.id, input); - - expect(result.name).toBe("New Role"); - expect(result.localizedNames).toEqual({ zh: "新角色", en: "New Role" }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should update only specified fields", async () => { - const created = await repository.create({ - name: "Original Name", - localizedNames: { zh: "原名" }, - }); - - const result = await repository.update(created.id, { name: "Updated Name" }); - - expect(result.name).toBe("Updated Name"); - expect(result.localizedNames).toEqual({ zh: "原名" }); - }); - }); - - describe("softDelete", () => { - test("should soft delete a role", async () => { - const created = await repository.create({ name: "Role to Delete" }); - - await repository.softDelete(created.id); - - const result = await repository.getById(created.id); - expect(result).toBeNull(); - }); - }); -}); diff --git a/packages/core/tests/integration/EngineRepository.test.ts b/packages/core/tests/integration/EngineRepository.test.ts deleted file mode 100644 index 21efcfd..0000000 --- a/packages/core/tests/integration/EngineRepository.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - engineRepository, - type CreateEngineRequestDto, - type UpdateEngineRequestDto, -} from "@cvsa/core"; - -const repository = engineRepository; - -describe("EngineRepository Integration Tests", () => { - describe("create", () => { - test("should create an engine with all fields", async () => { - const input: CreateEngineRequestDto = { - name: "VOCALOID", - description: "The latest version of VOCALOID synthesis engine", - localizedDescriptions: { - en: "The latest version of VOCALOID", - ja: "VOCALOIDの最新版", - zh: "VOCALOID最新版", - }, - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("VOCALOID"); - expect(result.description).toBe("The latest version of VOCALOID synthesis engine"); - expect(result.localizedDescriptions).toEqual({ - en: "The latest version of VOCALOID", - ja: "VOCALOIDの最新版", - zh: "VOCALOID最新版", - }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should create an engine with minimal fields (name only)", async () => { - const input: CreateEngineRequestDto = { - name: "Synthesizer V", - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("Synthesizer V"); - expect(result.description).toBeNull(); - expect(result.localizedDescriptions).toBeNull(); - }); - }); - - describe("getById", () => { - test("should return engine when exists", async () => { - const created = await repository.create({ name: "UTAU" }); - const result = await repository.getById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("UTAU"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when engine does not exist", async () => { - const result = await repository.getById(999999); - expect(result).toBeNull(); - }); - }); - - describe("getDetailsById", () => { - test("should return engine details when exists", async () => { - const created = await repository.create({ - name: "ACE Studio", - description: "AI-powered singing synthesis engine", - }); - const result = await repository.getDetailsById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("ACE Studio"); - expect(result?.description).toBe("AI-powered singing synthesis engine"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when engine does not exist", async () => { - const result = await repository.getDetailsById(999999); - expect(result).toBeNull(); - }); - }); - - describe("update", () => { - test("should update all fields", async () => { - const created = await repository.create({ - name: "Old Engine", - description: "Old description", - }); - - const input: UpdateEngineRequestDto = { - name: "Updated Engine", - description: "Updated description", - localizedDescriptions: { - en: "Updated", - }, - }; - - const result = await repository.update(created.id, input); - - expect(result.name).toBe("Updated Engine"); - expect(result.description).toBe("Updated description"); - expect(result.localizedDescriptions).toEqual({ en: "Updated" }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should update only specified fields", async () => { - const created = await repository.create({ - name: "Original Engine", - description: "Original description", - localizedDescriptions: { zh: "原始描述" }, - }); - - const result = await repository.update(created.id, { name: "Renamed Engine" }); - - expect(result.name).toBe("Renamed Engine"); - expect(result.description).toBe("Original description"); - expect(result.localizedDescriptions).toEqual({ zh: "原始描述" }); - }); - }); - - describe("softDelete", () => { - test("should soft delete an engine", async () => { - const created = await repository.create({ name: "Engine to Delete" }); - - await repository.softDelete(created.id); - - const result = await repository.getById(created.id); - expect(result).toBeNull(); - }); - }); -}); diff --git a/packages/core/tests/integration/OutboxRepository.test.ts b/packages/core/tests/integration/OutboxRepository.test.ts deleted file mode 100644 index 972601b..0000000 --- a/packages/core/tests/integration/OutboxRepository.test.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - outboxRepository, - type CreateOutboxEntryDto, - type PendingOutboxQueryDto, -} from "@cvsa/core"; - -const repository = outboxRepository; - -describe("OutboxRepository Integration Tests", () => { - describe("create", () => { - test("should create an outbox entry with all fields", async () => { - const input: CreateOutboxEntryDto = { - aggregateType: "Song", - aggregateId: 1, - eventType: "SongCreated", - payload: { title: "测试歌曲", artist: "测试歌手" }, - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.aggregateType).toBe("Song"); - expect(result.aggregateId).toBe(1); - expect(result.eventType).toBe("SongCreated"); - expect(result.payload).toEqual({ title: "测试歌曲", artist: "测试歌手" }); - expect(result.status).toBe("PENDING"); - expect(result.retryCount).toBe(0); - expect(result.lastError).toBeNull(); - expect(result.nextRetryAt).toBeNull(); - expect(result.createdAt).toBeDefined(); - expect(result.processedAt).toBeNull(); - }); - - test("should create an outbox entry with minimal fields (no payload)", async () => { - const input: CreateOutboxEntryDto = { - aggregateType: "Artist", - aggregateId: 2, - eventType: "ArtistUpdated", - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.aggregateType).toBe("Artist"); - expect(result.aggregateId).toBe(2); - expect(result.eventType).toBe("ArtistUpdated"); - expect(result.payload).toBeNull(); - expect(result.status).toBe("PENDING"); - expect(result.retryCount).toBe(0); - }); - - test("should default status to PENDING for new entries", async () => { - const result = await repository.create({ - aggregateType: "Album", - aggregateId: 3, - eventType: "AlbumCreated", - }); - - expect(result.status).toBe("PENDING"); - }); - }); - - describe("findPending", () => { - test("should return entries with PENDING status whose nextRetryAt is null or in the past", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 10, - eventType: "SongCreated", - }); - - const query: PendingOutboxQueryDto = { limit: 100 }; - const results = await repository.findPending(query); - - expect(results.length).toBeGreaterThanOrEqual(1); - const found = results.find((r) => r.id === created.id); - expect(found).toBeDefined(); - expect(found?.status).toBe("PENDING"); - }); - - test("should respect the limit parameter", async () => { - await repository.create({ - aggregateType: "Song", - aggregateId: 100, - eventType: "SongCreated", - }); - await repository.create({ - aggregateType: "Song", - aggregateId: 101, - eventType: "SongCreated", - }); - await repository.create({ - aggregateType: "Song", - aggregateId: 102, - eventType: "SongCreated", - }); - - const query: PendingOutboxQueryDto = { limit: 2 }; - const results = await repository.findPending(query); - - expect(results.length).toBeLessThanOrEqual(2); - }); - - test("should not return entries that are PROCESSED", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 200, - eventType: "SongCreated", - }); - await repository.markProcessed(created.id); - - const query: PendingOutboxQueryDto = { limit: 100 }; - const results = await repository.findPending(query); - - const found = results.find((r) => r.id === created.id); - expect(found).toBeUndefined(); - }); - - test("should not return entries that are PROCESSING", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 300, - eventType: "SongCreated", - }); - await repository.markProcessing(created.id); - - const query: PendingOutboxQueryDto = { limit: 100 }; - const results = await repository.findPending(query); - - const found = results.find((r) => r.id === created.id); - expect(found).toBeUndefined(); - }); - - test("should not return FAILED entries regardless of nextRetryAt", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 400, - eventType: "SongCreated", - }); - const pastRetry = new Date(Date.now() - 10000); - await repository.markFailed(created.id, "Test error", pastRetry); - - const query: PendingOutboxQueryDto = { limit: 100 }; - const results = await repository.findPending(query); - - const found = results.find((r) => r.id === created.id); - expect(found).toBeUndefined(); - }); - }); - - describe("markProcessing", () => { - test("should change status from PENDING to PROCESSING", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 20, - eventType: "SongCreated", - }); - - const result = await repository.markProcessing(created.id); - - expect(result).toBeDefined(); - expect(result.id).toBe(created.id); - expect(result.status).toBe("PROCESSING"); - }); - - test("should update an existing FAILED entry to PROCESSING", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 21, - eventType: "SongCreated", - }); - await repository.markFailed(created.id, "Previous error", new Date(Date.now() - 1000)); - - const result = await repository.markProcessing(created.id); - - expect(result.status).toBe("PROCESSING"); - }); - }); - - describe("markProcessed", () => { - test("should change status to PROCESSED and set processedAt", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 30, - eventType: "SongCreated", - }); - - const result = await repository.markProcessed(created.id); - - expect(result).toBeDefined(); - expect(result.id).toBe(created.id); - expect(result.status).toBe("PROCESSED"); - expect(result.processedAt).toBeDefined(); - expect(result.processedAt).not.toBeNull(); - }); - - test("should not appear in findPending after being processed", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 31, - eventType: "SongCreated", - }); - await repository.markProcessed(created.id); - - const query: PendingOutboxQueryDto = { limit: 100 }; - const results = await repository.findPending(query); - - const found = results.find((r) => r.id === created.id); - expect(found).toBeUndefined(); - }); - }); - - describe("markFailed", () => { - test("should set status to FAILED with lastError and nextRetryAt", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 40, - eventType: "SongCreated", - }); - const nextRetryAt = new Date(Date.now() + 60000); - - const result = await repository.markFailed( - created.id, - "Processing failed due to network error", - nextRetryAt - ); - - expect(result).toBeDefined(); - expect(result.id).toBe(created.id); - expect(result.status).toBe("FAILED"); - expect(result.lastError).toBe("Processing failed due to network error"); - expect(result.nextRetryAt).toBeDefined(); - expect(result.nextRetryAt).not.toBeNull(); - }); - - test("should increment retryCount when marking as failed", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 41, - eventType: "SongCreated", - }); - - const firstFail = await repository.markFailed( - created.id, - "First failure", - new Date(Date.now() + 60000) - ); - expect(firstFail.retryCount).toBe(1); - - const secondFail = await repository.markFailed( - created.id, - "Second failure", - new Date(Date.now() + 120000) - ); - expect(secondFail.retryCount).toBe(2); - }); - - test("should retain lastError from latest failure", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 42, - eventType: "SongCreated", - }); - - await repository.markFailed(created.id, "First error", new Date(Date.now() + 60000)); - const result = await repository.markFailed( - created.id, - "Second error", - new Date(Date.now() + 120000) - ); - - expect(result.lastError).toBe("Second error"); - }); - }); - - describe("full lifecycle", () => { - test("should support PENDING -> PROCESSING -> PROCESSED lifecycle", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 50, - eventType: "SongCreated", - }); - - expect(created.status).toBe("PENDING"); - - const processing = await repository.markProcessing(created.id); - expect(processing.status).toBe("PROCESSING"); - - const processed = await repository.markProcessed(created.id); - expect(processed.status).toBe("PROCESSED"); - expect(processed.processedAt).toBeDefined(); - }); - - test("should support PENDING -> PROCESSING -> FAILED -> PROCESSING -> PROCESSED lifecycle", async () => { - const created = await repository.create({ - aggregateType: "Song", - aggregateId: 51, - eventType: "SongCreated", - }); - - const processing = await repository.markProcessing(created.id); - expect(processing.status).toBe("PROCESSING"); - - const failed = await repository.markFailed( - created.id, - "Transient error", - new Date(Date.now() - 1000) - ); - expect(failed.status).toBe("FAILED"); - expect(failed.retryCount).toBe(1); - - const retryProcessing = await repository.markProcessing(created.id); - expect(retryProcessing.status).toBe("PROCESSING"); - - const processed = await repository.markProcessed(created.id); - expect(processed.status).toBe("PROCESSED"); - expect(processed.processedAt).toBeDefined(); - }); - }); -}); diff --git a/packages/core/tests/integration/SingerRepository.test.ts b/packages/core/tests/integration/SingerRepository.test.ts deleted file mode 100644 index 3b9a841..0000000 --- a/packages/core/tests/integration/SingerRepository.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - singerRepository, - type CreateSingerRequestDto, - type UpdateSingerRequestDto, -} from "@cvsa/core"; - -const repository = singerRepository; - -describe("SingerRepository Integration Tests", () => { - describe("create", () => { - test("should create a singer with all fields", async () => { - const input: CreateSingerRequestDto = { - name: "洛天依", - avatarUrl: "https://example.com/luotianyi.jpg", - localizedNames: { ja: "洛天依", en: "Luo Tianyi" }, - language: "zh", - description: "A famous Chinese virtual singer developed by Shanghai Henian", - localizedDescriptions: { - en: "A famous Chinese virtual singer", - ja: "有名な中国のバーチャルシンガー", - }, - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("洛天依"); - expect(result.avatarUrl).toBe("https://example.com/luotianyi.jpg"); - expect(result.localizedNames).toEqual({ ja: "洛天依", en: "Luo Tianyi" }); - expect(result.language).toBe("zh"); - expect(result.description).toBe( - "A famous Chinese virtual singer developed by Shanghai Henian" - ); - expect(result.localizedDescriptions).toEqual({ - en: "A famous Chinese virtual singer", - ja: "有名な中国のバーチャルシンガー", - }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should create a singer with minimal fields", async () => { - const input: CreateSingerRequestDto = {}; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBeNull(); - expect(result.language).toBe("zh"); - expect(result.description).toBeNull(); - expect(result.localizedNames).toBeNull(); - expect(result.localizedDescriptions).toBeNull(); - }); - }); - - describe("getById", () => { - test("should return singer when exists", async () => { - const created = await repository.create({ name: "乐正绫" }); - const result = await repository.getById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("乐正绫"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when singer does not exist", async () => { - const result = await repository.getById(999999); - expect(result).toBeNull(); - }); - }); - - describe("getDetailsById", () => { - test("should return singer details when exists", async () => { - const created = await repository.create({ - name: "言和", - description: "Voicebank of YANHE", - }); - const result = await repository.getDetailsById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("言和"); - expect(result?.description).toBe("Voicebank of YANHE"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when singer does not exist", async () => { - const result = await repository.getDetailsById(999999); - expect(result).toBeNull(); - }); - }); - - describe("update", () => { - test("should update all fields", async () => { - const created = await repository.create({ - name: "Old Name", - language: "zh", - description: "Old description", - }); - - const input: UpdateSingerRequestDto = { - name: "New Name", - avatarUrl: "https://example.com/new.jpg", - language: "ja", - description: "Updated description", - localizedNames: { en: "New Name EN" }, - localizedDescriptions: { en: "Updated description EN" }, - }; - - const result = await repository.update(created.id, input); - - expect(result.name).toBe("New Name"); - expect(result.avatarUrl).toBe("https://example.com/new.jpg"); - expect(result.language).toBe("ja"); - expect(result.description).toBe("Updated description"); - expect(result.localizedNames).toEqual({ en: "New Name EN" }); - expect(result.localizedDescriptions).toEqual({ en: "Updated description EN" }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should update only specified fields", async () => { - const created = await repository.create({ - name: "Original Name", - language: "zh", - description: "Original description", - }); - - const result = await repository.update(created.id, { name: "Updated Name" }); - - expect(result.name).toBe("Updated Name"); - expect(result.language).toBe("zh"); - expect(result.description).toBe("Original description"); - }); - }); - - describe("softDelete", () => { - test("should soft delete a singer", async () => { - const created = await repository.create({ name: "Singer to Delete" }); - - await repository.softDelete(created.id); - - const result = await repository.getById(created.id); - expect(result).toBeNull(); - }); - }); -}); diff --git a/packages/core/tests/integration/SongRepository.test.ts b/packages/core/tests/integration/SongRepository.test.ts deleted file mode 100644 index c72c9fc..0000000 --- a/packages/core/tests/integration/SongRepository.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { songRepository, type CreateSongRequestDto, type UpdateSongRequestDto } from "@cvsa/core"; -import { prisma } from "@cvsa/db"; - -const repository = songRepository; - -describe("SongRepository Integration Tests", () => { - describe("create", () => { - test("should create a song with all fields", async () => { - const input: CreateSongRequestDto = { - type: "ORIGINAL", - name: "Test Song", - duration: 180, - description: "A test song", - coverUrl: "https://example.com/cover.jpg", - publishedAt: new Date("2024-01-01").toISOString(), - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.type).toBe("ORIGINAL"); - expect(result.name).toBe("Test Song"); - expect(result.duration).toBe(180); - expect(result.description).toBe("A test song"); - expect(result.coverUrl).toBe("https://example.com/cover.jpg"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should create a song with minimal fields", async () => { - const input: CreateSongRequestDto = { - name: "Minimal Song", - }; - - const result = await repository.create(input); - - expect(result).toBeDefined(); - expect(result.id).toBeGreaterThan(0); - expect(result.name).toBe("Minimal Song"); - expect(result.type).toBeNull(); - expect(result.duration).toBeNull(); - }); - }); - - describe("getById", () => { - test("should return song when exists", async () => { - const created = await repository.create({ name: "Find Me Song" }); - const result = await repository.getById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("Find Me Song"); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when song does not exist", async () => { - const result = await repository.getById(999999); - expect(result).toBeNull(); - }); - }); - - describe("getDetailsById", () => { - test("should return song when exists", async () => { - const singer = await prisma.singer.create({ - data: { - name: "赤羽", - }, - }); - const artist = await prisma.artist.create({ - data: { - name: "月华P", - }, - }); - const artistRole = await prisma.artistRole.create({ - data: { - name: "作曲", - }, - }); - - const created = await repository.create({ - name: "尘海绘仙缘", - lyrics: [ - { - plainText: "捻笔指掌中 遥看日月变", - }, - ], - performances: [ - { - singerId: singer.id, - }, - ], - creations: [ - { - artistId: artist.id, - roleId: artistRole.id, - }, - ], - }); - const result = await repository.getDetailsById(created.id); - - expect(result).toBeDefined(); - expect(result?.id).toBe(created.id); - expect(result?.name).toBe("尘海绘仙缘"); - expect(result?.lyrics).toHaveLength(1); - expect(result?.lyrics[0]).toMatchObject({ - plainText: "捻笔指掌中 遥看日月变", - }); - expect(result?.artists).toHaveLength(1); - expect(result?.artists[0]).toMatchObject({ - name: "月华P", - role: { - name: "作曲", - }, - }); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result?.deletedAt).not.toBeDefined(); - }); - - test("should return null when song does not exist", async () => { - const result = await repository.getDetailsById(999999); - expect(result).toBeNull(); - }); - }); - - describe("update", () => { - test("should update all fields", async () => { - const created = await repository.create({ name: "Old Name", type: "ORIGINAL" }); - - const input: UpdateSongRequestDto = { - name: "New Name", - type: "COVER", - duration: 200, - }; - - const result = await repository.update(created.id, input); - - expect(result.name).toBe("New Name"); - expect(result.type).toBe("COVER"); - expect(result.duration).toBe(200); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(result.deletedAt).not.toBeDefined(); - }); - - test("should update only specified fields", async () => { - const created = await repository.create({ - name: "Original Name", - type: "ORIGINAL", - duration: 100, - }); - - const result = await repository.update(created.id, { name: "Updated Name" }); - - expect(result.name).toBe("Updated Name"); - expect(result.type).toBe("ORIGINAL"); - expect(result.duration).toBe(100); - }); - }); - - describe("softDelete", () => { - test("should soft delete a song", async () => { - const created = await repository.create({ name: "Song to Delete" }); - - await repository.softDelete(created.id); - - const result = await repository.getById(created.id); - expect(result).toBeNull(); - }); - }); - - describe("createLyrics", () => { - test("should create lyrics for a song", async () => { - const song = await repository.create({ name: "Song with Lyrics" }); - - const lyric = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "歌词内容", - }); - - expect(lyric).toBeDefined(); - expect(lyric.id).toBeGreaterThan(0); - expect(lyric.language).toBe("zh"); - expect(lyric.isTranslated).toBe(false); - expect(lyric.plainText).toBe("歌词内容"); - expect(lyric.ttml).toBeNull(); - expect(lyric.lrc).toBeNull(); - //@ts-expect-error accessing nonexistent field for testing purpose - expect(lyric.deletedAt).not.toBeDefined(); - }); - - test("should create lyrics with optional fields", async () => { - const song = await repository.create({ name: "Song with Full Lyrics" }); - - const lyric = await repository.createLyrics(song.id, { - language: "ja", - isTranslated: true, - plainText: "日本語の歌詞", - ttml: "test", - lrc: "[00:00.00]test", - }); - - expect(lyric.language).toBe("ja"); - expect(lyric.isTranslated).toBe(true); - expect(lyric.plainText).toBe("日本語の歌詞"); - expect(lyric.ttml).toBe("test"); - expect(lyric.lrc).toBe("[00:00.00]test"); - }); - }); - - describe("getLyricsBySongId", () => { - test("should return all lyrics for a song", async () => { - const song = await repository.create({ - name: "Song with Multiple Lyrics", - }); - - await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "中文歌词", - }); - await repository.createLyrics(song.id, { - language: "en", - isTranslated: true, - plainText: "English lyrics", - }); - - const lyrics = await repository.getLyricsBySongId(song.id); - - expect(lyrics).toHaveLength(2); - expect(lyrics.map((l) => l.language)).toContain("zh"); - expect(lyrics.map((l) => l.language)).toContain("en"); - }); - - test("should return empty array when song has no lyrics", async () => { - const song = await repository.create({ name: "Song without Lyrics" }); - - const lyrics = await repository.getLyricsBySongId(song.id); - - expect(lyrics).toEqual([]); - }); - - test("should not return deleted lyrics", async () => { - const song = await repository.create({ name: "Song with Deleted Lyrics" }); - - const lyric = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "将被删除的歌词", - }); - await repository.softDeleteLyric(lyric.id); - - const lyrics = await repository.getLyricsBySongId(song.id); - - expect(lyrics).toHaveLength(0); - }); - }); - - describe("getLyricById", () => { - test("should return lyric when exists", async () => { - const song = await repository.create({ name: "Song" }); - const created = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "歌词", - }); - - const lyric = await repository.getLyricById(created.id); - - expect(lyric).toBeDefined(); - expect(lyric?.id).toBe(created.id); - expect(lyric?.plainText).toBe("歌词"); - }); - - test("should return null when lyric does not exist", async () => { - const lyric = await repository.getLyricById(999999); - - expect(lyric).toBeNull(); - }); - - test("should return null when lyric is deleted", async () => { - const song = await repository.create({ name: "Song" }); - const created = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "歌词", - }); - await repository.softDeleteLyric(created.id); - - const lyric = await repository.getLyricById(created.id); - - expect(lyric).toBeNull(); - }); - }); - - describe("updateLyric", () => { - test("should update all fields", async () => { - const song = await repository.create({ name: "Song" }); - const created = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "原歌词", - }); - - const updated = await repository.updateLyric(created.id, { - language: "en", - isTranslated: true, - plainText: "Updated lyrics", - ttml: "updated", - lrc: "[00:00.00]updated", - }); - - expect(updated.language).toBe("en"); - expect(updated.isTranslated).toBe(true); - expect(updated.plainText).toBe("Updated lyrics"); - expect(updated.ttml).toBe("updated"); - expect(updated.lrc).toBe("[00:00.00]updated"); - }); - - test("should update only specified fields", async () => { - const song = await repository.create({ name: "Song" }); - const created = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "原歌词", - ttml: "original", - }); - - const updated = await repository.updateLyric(created.id, { - plainText: "新歌词", - }); - - expect(updated.plainText).toBe("新歌词"); - expect(updated.language).toBe("zh"); - expect(updated.isTranslated).toBe(false); - expect(updated.ttml).toBe("original"); - }); - }); - - describe("softDeleteLyric", () => { - test("should soft delete a lyric", async () => { - const song = await repository.create({ name: "Song" }); - const created = await repository.createLyrics(song.id, { - language: "zh", - isTranslated: false, - plainText: "将被删除", - }); - - await repository.softDeleteLyric(created.id); - - const lyric = await repository.getLyricById(created.id); - expect(lyric).toBeNull(); - }); - }); -}); diff --git a/packages/core/tests/integration/SongSearchService.test.ts b/packages/core/tests/integration/SongSearchService.test.ts deleted file mode 100644 index 238e88a..0000000 --- a/packages/core/tests/integration/SongSearchService.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { songRepository } from "@cvsa/core"; -import { prisma } from "@cvsa/db"; -import { SongSearchService } from "../../src/search/catalog/song"; -import type { EmbeddingAppApi } from "@cvsa/embedding"; -import { SearchManager } from "../../src/search/manager"; -import { env } from "@cvsa/env"; -import { MeiliSearch } from "meilisearch"; - -const MEILI_HOST = env.MEILI_API_URL ?? "http://127.0.0.1:7700"; -const MEILI_MASTER_KEY = env.MEILI_MASTER_KEY ?? ""; - -const client = new MeiliSearch({ host: MEILI_HOST, apiKey: MEILI_MASTER_KEY }); -const searchManager = await SearchManager.create(); -const mockEmbeddingManager: EmbeddingAppApi = { - embeddings: { - post: async ({ texts }: { texts: string[] }) => { - const dimensions = 256; - const embeddings = texts.map(() => - Array.from({ length: dimensions }, () => Math.random()) - ); - return { data: { embeddings } } as never; - }, - }, -} as never; -const service = new SongSearchService(songRepository, searchManager, mockEmbeddingManager); - -describe("SongSearchService Integration Tests", () => { - describe("sync", () => { - test("syncs song to search index", async () => { - const singer = await prisma.singer.create({ - data: { name: "测试歌手", language: "zh" }, - }); - const artistRole = await prisma.artistRole.create({ - data: { name: "作曲" }, - }); - const artist = await prisma.artist.create({ - data: { name: "测试艺术家", language: "zh" }, - }); - - const song = await songRepository.create({ - name: "测试歌曲", - type: "ORIGINAL", - language: "zh", - description: "这是一首测试歌曲", - performances: [{ singerId: singer.id }], - creations: [{ artistId: artist.id, roleId: artistRole.id }], - }); - - await service.sync(song.id); - - const index = client.index("song_zh"); - const documents = await index.getDocuments(); - expect(documents.results.length).toBeGreaterThan(0); - - const doc = documents.results.find((d) => d.id === song.id); - expect(doc).toBeDefined(); - expect(doc?.name).toBe("测试歌曲"); - }); - - test("deletes song from search index when song is removed", async () => { - const song = await songRepository.create({ name: "待删除歌曲" }); - - await service.sync(song.id); - - const index = client.index("song_zh"); - let documents = await index.getDocuments(); - let doc = documents.results.find((d) => d.id === song.id); - expect(doc).toBeDefined(); - - await songRepository.softDelete(song.id); - - await service.sync(song.id); - - documents = await index.getDocuments(); - doc = documents.results.find((d) => d.id === song.id); - expect(doc).toBeUndefined(); - }); - - test("handles song with localized content", async () => { - const song = await songRepository.create({ - name: "多语言歌曲", - language: "zh", - localizedNames: { en: "Multi-language Song", ja: "多言語曲" }, - localizedDescriptions: { en: "English description" }, - }); - - await service.sync(song.id); - - const zhIndex = client.index("song_zh"); - const enIndex = client.index("song_en"); - - const zhDocs = await zhIndex.getDocuments(); - const enDocs = await enIndex.getDocuments(); - - const zhDoc = zhDocs.results.find((d) => d.id === song.id); - const enDoc = enDocs.results.find((d) => d.id === song.id); - - expect(zhDoc).toBeDefined(); - expect(enDoc).toBeDefined(); - expect(enDoc?.name).toBe("Multi-language Song"); - expect(enDoc?.description).toBe("English description"); - }); - }); - - describe("search", () => { - test("performs hybrid search", async () => { - const song = await songRepository.create({ - name: "混合搜索测试", - language: "zh", - description: "用于测试混合搜索功能", - }); - - await service.sync(song.id); - - const result = await service.search("混合搜索", "zh"); - - expect(result).toBeDefined(); - expect(result.hits).toBeDefined(); - }); - - test("throws error when search manager is not available", async () => { - const serviceWithoutManager = new SongSearchService( - songRepository, - undefined as never, - mockEmbeddingManager - ); - - expect(serviceWithoutManager.search("test")).rejects.toThrow( - "Search or embedding service not available" - ); - }); - }); -}); - -type SongSearchDocument = { - id: number; - name?: string; - description?: string; - singers?: string[]; - artists?: string[]; -}; diff --git a/packages/core/tests/unit/ArtistRoleService.test.ts b/packages/core/tests/unit/ArtistRoleService.test.ts deleted file mode 100644 index 60568cb..0000000 --- a/packages/core/tests/unit/ArtistRoleService.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { prisma } from "@cvsa/db"; -import { AppError } from "../../src/error/AppError"; -import { ArtistRoleService } from "../../src/modules/catalog/artistRole/service"; -import type { ArtistRoleDetailsResponseDto } from "../../src/modules/catalog/artistRole/dto"; -import type { IArtistRoleRepository } from "../../src/modules/catalog/artistRole/repository.interface"; -import type { OutboxService } from "../../src/modules/outbox/service"; -import { createMockRepository } from "../utils"; - -( - prisma as unknown as { - $transaction: (fn: (tx: unknown) => Promise) => Promise; - } -).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); - -const mockArtistRole: ArtistRoleDetailsResponseDto = { - id: 1, - name: "Producer", - language: "zh", - localizedNames: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; - -const mockOutboxEntry = { - id: 1, - aggregateType: "artistRole" as const, - aggregateId: 1, - eventType: "artistRole.created" as const, - payload: null, - status: "PENDING" as const, - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, -}; - -describe("ArtistRoleService", () => { - const mockRepository = createMockRepository({ - getById: async (id: number) => { - if (id === 1) { - return mockArtistRole; - } - return null; - }, - getDetailsById: async (id: number) => { - if (id === 1) { - return mockArtistRole; - } - return null; - }, - create: async () => mockArtistRole, - update: async () => mockArtistRole, - softDelete: async () => {}, - }); - - const mockOutboxService = { - createEntry: mock(async () => mockOutboxEntry), - enqueue: mock(async () => {}), - processEntry: mock(async () => {}), - recoverStaleEntries: mock(async () => {}), - }; - - const artistRoleService = new ArtistRoleService( - mockRepository as unknown as IArtistRoleRepository, - mockOutboxService as unknown as OutboxService - ); - - describe("getDetails", () => { - test("returns artist role when role exists", async () => { - const result = await artistRoleService.getDetails(1); - - expect(result).toEqual(mockArtistRole); - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when role does not exist", async () => { - mockRepository.getDetailsById.mockResolvedValueOnce(null); - - expect(artistRoleService.getDetails(999)).rejects.toThrow(AppError); - expect(artistRoleService.getDetails(999)).rejects.toThrow("error.artistRole.notfound"); - expect(artistRoleService.getDetails(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("create", () => { - const createInput = { - name: "Composer", - }; - - test("creates role and calls outbox.createEntry and outbox.enqueue", async () => { - const result = await artistRoleService.create(createInput); - - expect(result).toMatchObject({ - name: "Producer", - }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artistRole", - aggregateId: mockArtistRole.id, - eventType: "artistRole.created", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - }); - - describe("update", () => { - const updateInput = { name: "Lyricist" }; - - test("updates role and calls outbox.createEntry and outbox.enqueue on success", async () => { - const result = await artistRoleService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Producer", - }); - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artistRole", - aggregateId: 1, - eventType: "artistRole.updated", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when role does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(artistRoleService.update(999, updateInput)).rejects.toThrow(AppError); - expect(artistRoleService.update(999, updateInput)).rejects.toThrow( - "error.artistRole.notfound" - ); - expect(artistRoleService.update(999, updateInput)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("delete", () => { - test("soft deletes role and calls outbox.createEntry and outbox.enqueue on success", async () => { - await artistRoleService.delete(1); - - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artistRole", - aggregateId: 1, - eventType: "artistRole.deleted", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when role does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(artistRoleService.delete(999)).rejects.toThrow(AppError); - expect(artistRoleService.delete(999)).rejects.toThrow("error.artistRole.notfound"); - expect(artistRoleService.delete(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/ArtistSearchService.test.ts b/packages/core/tests/unit/ArtistSearchService.test.ts deleted file mode 100644 index e71742a..0000000 --- a/packages/core/tests/unit/ArtistSearchService.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; -import type { ArtistDetailsResponseDto } from "../../src/modules"; -import type { IRepositoryWithGetDetails } from "../../src/types/repository"; - -const mockGetLocalizedIndexesOfEntity = mock(); -const mockGetAdminIndex = mock(); -const mockGetSearchIndex = mock(); - -const mockAdminIndex = { - deleteDocument: mock(), - addDocuments: mock(), - search: mock(), -}; - -const mockSearchIndex = { - search: mock(), -}; - -const mockSearchManager = { - getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, - getAdminIndex: mockGetAdminIndex, - getSearchIndex: mockGetSearchIndex, - waitForTask: mock().mockResolvedValue(undefined), -}; - -const mockEmbeddingsPost = mock(); -const mockEmbeddingManager = { - embeddings: { - post: mockEmbeddingsPost, - }, -}; - -const { ArtistSearchService } = await import("../../src/search/catalog/artist"); - -const mockArtistDetails: ArtistDetailsResponseDto = { - id: 1, - name: "Test Artist", - language: "zh", - aliases: ["Alias1", "Alias2"], - description: "A test artist", - localizedNames: { en: "Test Artist EN", ja: "テストアーティスト" }, - localizedDescriptions: { en: "A test artist in English" }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - userId: null, -}; - -describe("ArtistSearchService", () => { - let service: InstanceType; - let mockRepository: IRepositoryWithGetDetails; - - beforeEach(() => { - mockGetLocalizedIndexesOfEntity.mockClear(); - mockGetAdminIndex.mockClear(); - mockGetSearchIndex.mockClear(); - mockAdminIndex.deleteDocument.mockClear(); - mockAdminIndex.addDocuments.mockClear(); - mockAdminIndex.search.mockClear(); - mockSearchIndex.search.mockClear(); - mockEmbeddingsPost.mockClear(); - - mockGetAdminIndex.mockResolvedValue(mockAdminIndex); - mockGetSearchIndex.mockResolvedValue(mockSearchIndex); - mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); - mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); - mockEmbeddingsPost.mockResolvedValue({ - data: { embeddings: [[0.1, 0.2, 0.3]] }, - }); - - mockRepository = { - getDetailsById: mock(), - }; - - service = new ArtistSearchService( - mockRepository, - mockSearchManager as never, - mockEmbeddingManager as never - ); - }); - - afterAll(() => { - mock.restore(); - mock.clearAllMocks(); - }); - - describe("sync", () => { - test("syncs artist to all language indexes", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockArtistDetails - ); - - await service.sync(1); - - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0] as unknown[]; - expect(callArgs[1]).toEqual({ primaryKey: "id" }); - }); - - test("deletes document from all indexes when artist not found", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); - mockGetLocalizedIndexesOfEntity.mockResolvedValue(["artist_zh", "artist_en"]); - - await service.sync(999); - - expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("artist"); - expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); - }); - - test("handles missing search manager gracefully", async () => { - const serviceWithoutManager = new ArtistSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockArtistDetails - ); - - await serviceWithoutManager.sync(1); - - expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); - }); - - test("builds document with localized content", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockArtistDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const allCalls = mockAdminIndex.addDocuments.mock.calls as unknown as [ - { id: number; name: string }[], - unknown, - ][]; - const enCall = allCalls.find((call) => call[0][0].name === "Test Artist EN"); - expect(enCall).toBeDefined(); - }); - - test("handles artist with no localized content", async () => { - const artistWithoutLocalization = { - ...mockArtistDetails, - localizedNames: {}, - localizedDescriptions: {}, - }; - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - artistWithoutLocalization - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - }); - - test("handles embedding generation failure", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockArtistDetails - ); - mockEmbeddingsPost.mockResolvedValue({ - data: null, - }); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const doc = ( - (mockAdminIndex.addDocuments.mock.calls[0] as unknown[])[0] as { - _vectors: { "potion-multilingual-128M": unknown }; - }[] - )[0]; - expect(doc._vectors).toEqual({ - "potion-multilingual-128M": null, - }); - }); - }); - - describe("search", () => { - test("performs hybrid search with embedding", async () => { - const mockSearchResult = { - hits: [{ id: 1, name: "Test Artist" }], - query: "test", - processingTimeMs: 30, - offset: 1, - limit: 1, - estimatedTotalHits: 1, - }; - mockSearchIndex.search.mockResolvedValue(mockSearchResult); - - const result = await service.search("test query", "zh"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("artist_zh"); - expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); - expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { - vector: [0.1, 0.2, 0.3], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, - showRankingScore: true, - }); - expect(result).toEqual(mockSearchResult); - }); - - test("uses default language when not specified", async () => { - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("artist_zh"); - }); - - test("throws when search manager not available", async () => { - const serviceWithoutManager = new ArtistSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - expect(serviceWithoutManager.search("test")).rejects.toThrow( - "Search or embedding service not available" - ); - }); - - test("handles missing embedding response gracefully", async () => { - mockEmbeddingsPost.mockResolvedValue({ data: null }); - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test", "zh"); - - expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { - vector: undefined, - hybrid: undefined, - showRankingScore: true, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/ArtistService.test.ts b/packages/core/tests/unit/ArtistService.test.ts deleted file mode 100644 index d9f4905..0000000 --- a/packages/core/tests/unit/ArtistService.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { prisma } from "@cvsa/db"; -import { AppError } from "../../src/error/AppError"; -import { ArtistService } from "../../src/modules/catalog/artist/service"; -import type { ArtistDetailsResponseDto } from "../../src/modules/catalog/artist/dto"; -import type { IArtistRepository } from "../../src/modules/catalog/artist/repository.interface"; -import type { OutboxService } from "../../src/modules/outbox/service"; -import { createMockRepository } from "../utils"; - -( - prisma as unknown as { - $transaction: (fn: (tx: unknown) => Promise) => Promise; - } -).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); - -const mockArtistDetails: ArtistDetailsResponseDto = { - id: 1, - name: "Test Artist", - localizedNames: null, - language: "zh", - aliases: [], - description: "A test artist", - localizedDescriptions: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - userId: null, -}; - -const mockOutboxEntry = { - id: 1, - aggregateType: "artist" as const, - aggregateId: 1, - eventType: "artist.created" as const, - payload: null, - status: "PENDING" as const, - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, -}; - -describe("ArtistService", () => { - const mockRepository = createMockRepository({ - getById: async (id: number) => { - if (id === 1) { - return mockArtistDetails; - } - return null; - }, - getDetailsById: async (id: number) => { - if (id === 1) { - return mockArtistDetails; - } - return null; - }, - create: async () => mockArtistDetails, - update: async () => mockArtistDetails, - softDelete: async () => {}, - }); - - const mockOutboxService = { - createEntry: mock(async () => mockOutboxEntry), - enqueue: mock(async () => {}), - processEntry: mock(async () => {}), - recoverStaleEntries: mock(async () => {}), - }; - - const artistService = new ArtistService( - mockRepository as unknown as IArtistRepository, - mockOutboxService as unknown as OutboxService - ); - - describe("getDetails", () => { - test("returns artist details when artist exists", async () => { - const result = await artistService.getDetails(1); - - expect(result).toEqual(mockArtistDetails); - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when artist does not exist", async () => { - mockRepository.getDetailsById.mockResolvedValueOnce(null); - - expect(artistService.getDetails(999)).rejects.toThrow(AppError); - expect(artistService.getDetails(999)).rejects.toThrow("error.artist.notfound"); - expect(artistService.getDetails(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("create", () => { - const createInput = { - name: "New Artist", - language: "ja", - }; - - test("creates artist and calls outbox.createEntry and outbox.enqueue", async () => { - const result = await artistService.create(createInput); - - expect(result).toMatchObject({ - name: "Test Artist", - language: "zh", - }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artist", - aggregateId: mockArtistDetails.id, - eventType: "artist.created", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - }); - - describe("update", () => { - const updateInput = { name: "Updated Artist" }; - - test("updates artist and calls outbox.createEntry and outbox.enqueue on success", async () => { - const result = await artistService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Test Artist", - language: "zh", - }); - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artist", - aggregateId: 1, - eventType: "artist.updated", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when artist does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(artistService.update(999, updateInput)).rejects.toThrow(AppError); - expect(artistService.update(999, updateInput)).rejects.toThrow("error.artist.notfound"); - expect(artistService.update(999, updateInput)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("delete", () => { - test("soft deletes artist and calls outbox.createEntry and outbox.enqueue on success", async () => { - await artistService.delete(1); - - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "artist", - aggregateId: 1, - eventType: "artist.deleted", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when artist does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(artistService.delete(999)).rejects.toThrow(AppError); - expect(artistService.delete(999)).rejects.toThrow("error.artist.notfound"); - expect(artistService.delete(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/EngineService.test.ts b/packages/core/tests/unit/EngineService.test.ts deleted file mode 100644 index 7f47005..0000000 --- a/packages/core/tests/unit/EngineService.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { AppError } from "../../src/error/AppError"; -import { EngineService } from "../../src/modules/catalog/engine/service"; -import type { EngineDetailsResponseDto } from "../../src/modules/catalog/engine/dto"; -import type { IEngineRepository } from "../../src/modules/catalog/engine/repository.interface"; -import { createMockRepository } from "../utils"; - -const mockEngineDetails: EngineDetailsResponseDto = { - id: 1, - name: "Test Engine", - description: "A test engine", - localizedDescriptions: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; - -describe("EngineService", () => { - const mockRepository = createMockRepository({ - getById: async (id: number) => { - if (id === 1) { - return mockEngineDetails; - } - return null; - }, - getDetailsById: async (id: number) => { - if (id === 1) { - return mockEngineDetails; - } - return null; - }, - create: async () => mockEngineDetails, - update: async () => mockEngineDetails, - softDelete: async () => {}, - }); - - const engineService = new EngineService(mockRepository as unknown as IEngineRepository); - - describe("getDetails", () => { - test("returns engine details when engine exists", async () => { - const result = await engineService.getDetails(1); - - expect(result).toEqual(mockEngineDetails); - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when engine does not exist", async () => { - mockRepository.getDetailsById.mockResolvedValueOnce(null); - - expect(engineService.getDetails(999)).rejects.toThrow(AppError); - expect(engineService.getDetails(999)).rejects.toThrow("error.engine.notfound"); - expect(engineService.getDetails(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("create", () => { - const createInput = { - name: "New Engine", - description: "A new test engine", - }; - - test("creates engine and returns result", async () => { - const result = await engineService.create(createInput); - - expect(result).toMatchObject({ - name: "Test Engine", - description: "A test engine", - }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput); - }); - }); - - describe("update", () => { - const updateInput = { name: "Updated Engine" }; - - test("updates engine on success", async () => { - const result = await engineService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Test Engine", - description: "A test engine", - }); - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput); - }); - - test("throws NOT_FOUND error when engine does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(engineService.update(999, updateInput)).rejects.toThrow(AppError); - expect(engineService.update(999, updateInput)).rejects.toThrow("error.engine.notfound"); - expect(engineService.update(999, updateInput)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("delete", () => { - test("soft deletes engine on success", async () => { - await engineService.delete(1); - - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when engine does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(engineService.delete(999)).rejects.toThrow(AppError); - expect(engineService.delete(999)).rejects.toThrow("error.engine.notfound"); - expect(engineService.delete(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/OutboxProcessor.test.ts b/packages/core/tests/unit/OutboxProcessor.test.ts deleted file mode 100644 index 7927068..0000000 --- a/packages/core/tests/unit/OutboxProcessor.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, expect, mock, test, beforeEach } from "bun:test"; -import { createOutboxProcessor, type OutboxProcessorDeps } from "../../src/outbox/processor"; -import type { OutboxEntryDto } from "@cvsa/core/internal"; - -function makeEntry(overrides?: Partial): OutboxEntryDto { - return { - id: 1, - aggregateType: "song", - aggregateId: 42, - eventType: "SongCreated", - payload: null, - status: "PENDING", - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, - ...overrides, - }; -} - -function makeJob(entry: OutboxEntryDto) { - return { data: entry } as never; -} - -describe("createOutboxProcessor", () => { - let capturedProcessorId: number | undefined; - let capturedCallback: ( - aggregateType: string, - aggregateId: number, - eventType: string - ) => Promise; - - const mockOutboxService = { - processEntry: mock( - async ( - id: number, - processor: ( - aggregateType: string, - aggregateId: number, - eventType: string - ) => Promise - ) => { - capturedProcessorId = id; - capturedCallback = processor; - } - ), - }; - - const mockSongSearch = { sync: mock(async () => {}) }; - const mockArtistSearch = { sync: mock(async () => {}) }; - - const deps: OutboxProcessorDeps = { - outboxService: mockOutboxService as unknown as never, - searchServices: { - song: mockSongSearch, - artist: mockArtistSearch, - }, - }; - - const processor = createOutboxProcessor(deps); - - beforeEach(() => { - mockOutboxService.processEntry.mockClear(); - mockSongSearch.sync.mockClear(); - mockArtistSearch.sync.mockClear(); - capturedProcessorId = undefined; - }); - - test("should call outboxService.processEntry with entry.id from job", async () => { - const entry = makeEntry({ id: 7 }); - const job = makeJob(entry); - - await processor(job); - - expect(capturedProcessorId).toBe(7); - expect(mockOutboxService.processEntry).toHaveBeenCalledTimes(1); - }); - - test("should dispatch to song search service for aggregateType 'song'", async () => { - const entry = makeEntry({ aggregateType: "song", aggregateId: 42 }); - const job = makeJob(entry); - - await processor(job); - - await capturedCallback("song", 42, "SongCreated"); - - expect(mockSongSearch.sync).toHaveBeenCalledWith(42); - expect(mockArtistSearch.sync).not.toHaveBeenCalled(); - }); - - test("should dispatch to artist search service for aggregateType 'artist'", async () => { - const entry = makeEntry({ aggregateType: "artist", aggregateId: 99 }); - const job = makeJob(entry); - - await processor(job); - - await capturedCallback("artist", 99, "ArtistUpdated"); - - expect(mockArtistSearch.sync).toHaveBeenCalledWith(99); - expect(mockSongSearch.sync).not.toHaveBeenCalled(); - }); - - test("should throw for unknown aggregateType", async () => { - const entry = makeEntry({ aggregateType: "unknown", aggregateId: 1 }); - const job = makeJob(entry); - - await processor(job); - - const promise = capturedCallback("unknown", 1, "UnknownEvent"); - - expect(promise).rejects.toThrow("Unknown aggregate type: unknown"); - expect(mockSongSearch.sync).not.toHaveBeenCalled(); - expect(mockArtistSearch.sync).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/core/tests/unit/OutboxService.test.ts b/packages/core/tests/unit/OutboxService.test.ts deleted file mode 100644 index 4d0d8ba..0000000 --- a/packages/core/tests/unit/OutboxService.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { describe, expect, mock, test, beforeEach } from "bun:test"; -import { OutboxService } from "@cvsa/core/internal"; -import { Prisma } from "@cvsa/db"; -import type { CreateOutboxEntryDto, OutboxEntryDto } from "@cvsa/core"; - -const mockOutboxEntry = (overrides?: Partial): OutboxEntryDto => ({ - id: 1, - aggregateType: "Song", - aggregateId: 42, - eventType: "SongCreated", - payload: null, - status: "PENDING", - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, - ...overrides, -}); - -function prismaNotFoundError(): Prisma.PrismaClientKnownRequestError { - return new Prisma.PrismaClientKnownRequestError("Not found", { - code: "P2025", - clientVersion: "test", - }); -} - -describe("OutboxService", () => { - const mockRepository = { - create: mock(async () => mockOutboxEntry()), - findPending: mock(async () => [] as OutboxEntryDto[]), - markProcessing: mock(async (id: number) => mockOutboxEntry({ id, status: "PROCESSING" })), - markProcessed: mock(async (id: number) => - mockOutboxEntry({ id, status: "PROCESSED", processedAt: new Date().toISOString() }) - ), - markFailed: mock(async (id: number, lastError: string, nextRetryAt: Date) => - mockOutboxEntry({ - id, - status: "FAILED", - lastError, - nextRetryAt: nextRetryAt.toISOString(), - retryCount: 1, - }) - ), - }; - - const mockQueue = { - add: mock(async () => {}), - }; - - const service = new OutboxService( - mockRepository as unknown as never, - mockQueue as unknown as never - ); - - beforeEach(() => { - mockRepository.create.mockClear(); - mockRepository.findPending.mockClear(); - mockRepository.markProcessing.mockClear(); - mockRepository.markProcessed.mockClear(); - mockRepository.markFailed.mockClear(); - mockQueue.add.mockClear(); - }); - - describe("createEntry", () => { - test("should delegate to repository.create with input and tx", async () => { - const input: CreateOutboxEntryDto = { - aggregateType: "Song", - aggregateId: 42, - eventType: "SongCreated", - }; - const mockTx = {} as never; - - const result = await service.createEntry(input, mockTx); - - expect(mockRepository.create).toHaveBeenCalledWith(input, mockTx); - expect(result.id).toBe(1); - expect(result.status).toBe("PENDING"); - }); - - test("should pass payload when provided", async () => { - const input: CreateOutboxEntryDto = { - aggregateType: "Artist", - aggregateId: 10, - eventType: "ArtistUpdated", - payload: { name: "test" }, - }; - const mockTx = {} as never; - - await service.createEntry(input, mockTx); - - expect(mockRepository.create).toHaveBeenCalledWith(input, mockTx); - }); - }); - - describe("enqueue", () => { - test("should add entry to queue with correct job name and options", async () => { - const entry = mockOutboxEntry(); - - await service.enqueue(entry); - - expect(mockQueue.add).toHaveBeenCalledTimes(1); - const callArgs = mockQueue.add.mock.calls[0] as unknown[]; - expect(callArgs[0]).toBe("outbox-Song-42"); - expect(callArgs[1]).toBe(entry); - expect(callArgs[2]).toEqual({ - jobId: "outbox-1", - attempts: 5, - backoff: { type: "exponential", delay: 1000 }, - }); - }); - }); - - describe("processEntry", () => { - test("should mark processing, call processor, then mark processed on success", async () => { - const processor = mock(async () => {}); - - await service.processEntry(1, processor); - - expect(mockRepository.markProcessing).toHaveBeenCalledWith(1); - expect(processor).toHaveBeenCalledWith("Song", 42, "SongCreated"); - expect(mockRepository.markProcessed).toHaveBeenCalledWith(1); - expect(mockRepository.markFailed).not.toHaveBeenCalled(); - }); - - test("should rethrow processor error when retryCount is below max retries", async () => { - mockRepository.markProcessing.mockResolvedValueOnce( - mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 2 }) - ); - const processorError = new Error("Processing failed"); - const processor = mock(async () => { - throw processorError; - }); - - const promise = service.processEntry(1, processor); - - expect(promise).rejects.toThrow("Processing failed"); - await promise.catch(() => {}); - expect(mockRepository.markProcessed).not.toHaveBeenCalled(); - expect(mockRepository.markFailed).not.toHaveBeenCalled(); - }); - - test("should mark failed when retryCount reaches max retries", async () => { - mockRepository.markProcessing.mockResolvedValueOnce( - mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 4 }) - ); - const processor = mock(async () => { - throw new Error("Final failure"); - }); - - await service.processEntry(1, processor); - - expect(mockRepository.markFailed).toHaveBeenCalledTimes(1); - const failArgs = mockRepository.markFailed.mock.calls[0] as unknown[]; - expect(failArgs[0]).toBe(1); - expect(failArgs[1]).toBe("Final failure"); - expect(mockRepository.markProcessed).not.toHaveBeenCalled(); - }); - - test("should convert non-Error throws to string message", async () => { - mockRepository.markProcessing.mockResolvedValueOnce( - mockOutboxEntry({ id: 1, status: "PROCESSING", retryCount: 4 }) - ); - const processor = mock(async () => { - throw "raw string error"; - }); - - await service.processEntry(1, processor); - - expect(mockRepository.markFailed).toHaveBeenCalledTimes(1); - const failArgs = mockRepository.markFailed.mock.calls[0] as unknown[]; - expect(failArgs[1]).toBe("raw string error"); - }); - - test("should silently skip when entry is not found (P2025)", async () => { - mockRepository.markProcessing.mockRejectedValueOnce(prismaNotFoundError()); - const processor = mock(async () => {}); - - const promise = service.processEntry(1, processor); - - expect(promise).resolves.toBeUndefined(); - await promise; - expect(processor).not.toHaveBeenCalled(); - }); - - test("should rethrow non-P2025 errors from markProcessing", async () => { - const otherError = new Error("DB connection failed"); - mockRepository.markProcessing.mockRejectedValueOnce(otherError); - const processor = mock(async () => {}); - - const promise = service.processEntry(1, processor); - - expect(promise).rejects.toThrow("DB connection failed"); - await promise.catch(() => {}); - expect(processor).not.toHaveBeenCalled(); - }); - }); - - describe("recoverStaleEntries", () => { - test("should find pending entries and enqueue each one", async () => { - const entries = [ - mockOutboxEntry({ id: 1 }), - mockOutboxEntry({ id: 2 }), - mockOutboxEntry({ id: 3 }), - ]; - mockRepository.findPending.mockResolvedValueOnce(entries); - - await service.recoverStaleEntries(50); - - expect(mockRepository.findPending).toHaveBeenCalledWith({ limit: 50 }); - expect(mockQueue.add).toHaveBeenCalledTimes(3); - const firstCall = mockQueue.add.mock.calls[0] as unknown as [ - string, - OutboxEntryDto, - unknown, - ]; - const secondCall = mockQueue.add.mock.calls[1] as unknown as [ - string, - OutboxEntryDto, - unknown, - ]; - const thirdCall = mockQueue.add.mock.calls[2] as unknown as [ - string, - OutboxEntryDto, - unknown, - ]; - expect(firstCall[0]).toBe("outbox-recover-1"); - expect(secondCall[0]).toBe("outbox-recover-2"); - expect(thirdCall[0]).toBe("outbox-recover-3"); - }); - - test("should use default limit of 100", async () => { - mockRepository.findPending.mockResolvedValueOnce([]); - - await service.recoverStaleEntries(); - - expect(mockRepository.findPending).toHaveBeenCalledWith({ limit: 100 }); - }); - - test("should handle empty pending entries", async () => { - mockRepository.findPending.mockResolvedValueOnce([]); - - await service.recoverStaleEntries(); - - expect(mockQueue.add).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/core/tests/unit/SearchManager.test.ts b/packages/core/tests/unit/SearchManager.test.ts deleted file mode 100644 index 8225f66..0000000 --- a/packages/core/tests/unit/SearchManager.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { describe, expect, mock, test, beforeEach } from "bun:test"; -import type { Index, RecordAny, Task, MeiliSearch as MeiliSearchType } from "meilisearch"; -import { SearchManager } from "../../src/search/manager"; - -interface MockClient { - index: ReturnType; - getIndexes: ReturnType; - getKeys?: ReturnType; - createKey?: ReturnType; - tasks?: { - waitForTask: ReturnType; - getTask: ReturnType; - }; -} - -const mockGetKeys = mock(); -const mockCreateKey = mock(); -const mockGetIndexes = mock(); -const mockWaitForTask = mock(); -const mockGetTask = mock(); -const mockIndexGetSettings = mock(); -const mockIndexResetSettings = mock(() => { - return { taskUid: 1 }; -}); -const mockIndexUpdateSettings = mock(() => { - return { taskUid: 1 }; -}); -const mockAdminDeleteIndexIfExists = mock(); -const mockAdminCreateIndex = mock(); - -const mockIndexInstance = { - getSettings: mockIndexGetSettings, - resetSettings: mockIndexResetSettings, - updateSettings: mockIndexUpdateSettings, - deleteDocument: mock(), - addDocuments: mock(), - search: mock(), -}; - -const createMockIndex = (uid: string, getSettingsReturn?: Record) => { - const settingsMock = mock(); - settingsMock.mockResolvedValue( - getSettingsReturn ?? { - searchableAttributes: [], - filterableAttributes: [], - sortableAttributes: [], - rankingRules: [], - } - ); - return { - uid, - getSettings: settingsMock, - resetSettings: mockIndexResetSettings, - updateSettings: mockIndexUpdateSettings, - deleteDocument: mock(), - addDocuments: mock(), - search: mock(), - }; -}; - -const createMockClient = (): { client: MockClient; adminClient: MockClient } => { - const client = { - index: mock(() => mockIndexInstance), - getIndexes: mockGetIndexes, - }; - const adminClient = { - index: mock(() => mockIndexInstance), - getIndexes: mockGetIndexes, - getKeys: mockGetKeys, - createKey: mockCreateKey, - deleteIndexIfExists: mockAdminDeleteIndexIfExists, - createIndex: mockAdminCreateIndex, - tasks: { - waitForTask: mockWaitForTask, - getTask: mockGetTask, - }, - }; - return { client, adminClient }; -}; - -describe("SearchManager", () => { - beforeEach(() => { - mockGetKeys.mockClear(); - mockCreateKey.mockClear(); - mockGetIndexes.mockClear(); - mockIndexGetSettings.mockClear(); - mockIndexResetSettings.mockClear(); - mockIndexUpdateSettings.mockClear(); - mockWaitForTask.mockClear(); - mockGetTask.mockClear(); - mockAdminDeleteIndexIfExists.mockClear(); - mockAdminCreateIndex.mockClear(); - }); - - describe("create with custom clients", () => { - test("creates instance with provided clients", async () => { - const { client, adminClient } = createMockClient(); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - expect(manager).toBeDefined(); - }); - }); - - describe("getLocalizedIndexesOfEntity", () => { - test("returns indexes matching entity name", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [ - createMockIndex("song_zh-CN"), - createMockIndex("song_en"), - createMockIndex("artist_zh-CN"), - createMockIndex("no-locale"), - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const indexes = await manager.getLocalizedIndexesOfEntity("song"); - - expect(indexes).toEqual(["song_zh-CN", "song_en"]); - }); - - test("returns empty array when no matching indexes", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [createMockIndex("artist_zh-CN")], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const indexes = await manager.getLocalizedIndexesOfEntity("song"); - - expect(indexes).toEqual([]); - }); - }); - - describe("syncAllSettings", () => { - test("syncs settings when they differ", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [ - createMockIndex("song_zh-CN", { - searchableAttributes: ["old"], - filterableAttributes: [], - sortableAttributes: [], - rankingRules: [], - }), - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.syncAllSettings(); - - expect(mockIndexResetSettings).toHaveBeenCalled(); - expect(mockIndexUpdateSettings).toHaveBeenCalled(); - }); - - test("skips sync when settings match", async () => { - const { client, adminClient } = createMockClient(); - const matchingSettings = { - searchableAttributes: [ - "name", - "lyrics", - "description", - "singers", - "artists", - "bilibiliAid", - "bilibiliBvid", - ], - filterableAttributes: ["type", "tags", "singers", "engine"], - sortableAttributes: ["publishedAt", "bilibiliViews"], - rankingRules: ["attribute", "words", "proximity", "exactness", "typo", "sort"], - embedders: { - "potion-multilingual-128M": { - source: "userProvided", - dimensions: 256, - }, - }, - }; - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [createMockIndex("song_zh-CN", matchingSettings)], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.syncAllSettings(); - - expect(mockIndexResetSettings).not.toHaveBeenCalled(); - expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); - }); - - test("skips indexes without locale suffix", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [createMockIndex("song")], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.syncAllSettings(); - - expect(mockIndexResetSettings).not.toHaveBeenCalled(); - expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); - }); - - test("skips indexes not in INDEX_SETTINGS", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [createMockIndex("unknown_zh-CN")], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.syncAllSettings(); - - expect(mockIndexResetSettings).not.toHaveBeenCalled(); - expect(mockIndexUpdateSettings).not.toHaveBeenCalled(); - }); - }); - - describe("getAdminIndex", () => { - test("returns admin index instance", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const index = await manager.getAdminIndex("song_zh-CN"); - - expect(index).toBeDefined(); - }); - }); - - describe("getSearchIndex", () => { - test("returns search index instance", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const index = await manager.getSearchIndex("song_zh-CN"); - - expect(index).toBeDefined(); - }); - }); - - describe("listIndexes", () => { - test("returns list of all indexes", async () => { - const { client, adminClient } = createMockClient(); - const mockIndexes = [ - createMockIndex("song_zh-CN"), - createMockIndex("song_en"), - ] as unknown as Index[]; - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ results: mockIndexes }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const indexes = await manager.listIndexes(); - - expect(indexes.results).toEqual(mockIndexes); - }); - }); - - describe("waitForTask", () => { - test("waits for task completion", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - const mockTask = { uid: 123, status: "succeeded" } as Task; - mockWaitForTask.mockResolvedValue(mockTask); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const task = await manager.waitForTask(123); - - expect(task).toEqual(mockTask); - expect(mockWaitForTask).toHaveBeenCalledWith(123, { - timeout: 10000, - interval: 100, - }); - }); - }); - - describe("getTask", () => { - test("returns task details", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - const mockTask = { uid: 123, status: "succeeded" } as Task; - mockGetTask.mockResolvedValue(mockTask); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const task = await manager.getTask(123); - - expect(task).toEqual(mockTask); - }); - }); - - describe("clearAllIndex", () => { - test("deletes all existing indexes", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ - results: [createMockIndex("song_zh-CN"), createMockIndex("artist_en")], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.clearAllIndex(); - - expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledTimes(2); - expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledWith("song_zh-CN"); - expect(mockAdminDeleteIndexIfExists).toHaveBeenCalledWith("artist_en"); - }); - - test("handles empty index list gracefully", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - mockGetIndexes.mockResolvedValue({ results: [] }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.clearAllIndex(); - - expect(mockAdminDeleteIndexIfExists).not.toHaveBeenCalled(); - }); - }); - - describe("createIndex", () => { - test("creates a new index via admin client", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - await manager.createIndex("album_zh"); - - expect(mockAdminCreateIndex).toHaveBeenCalledWith("album_zh", undefined); - }); - - test("passes options to createIndex when provided", async () => { - const { client, adminClient } = createMockClient(); - - mockGetKeys.mockResolvedValue({ - results: [ - { key: "admin-key", actions: ["*"], indexes: ["*"] }, - { key: "search-key", actions: ["search"], indexes: ["*"] }, - ], - }); - - const manager = await SearchManager.create( - client as unknown as MeiliSearchType, - adminClient as unknown as MeiliSearchType - ); - const options = { primaryKey: "id" }; - await manager.createIndex("album_zh", options); - - expect(mockAdminCreateIndex).toHaveBeenCalledWith("album_zh", options); - }); - }); -}); diff --git a/packages/core/tests/unit/SingerSearchService.test.ts b/packages/core/tests/unit/SingerSearchService.test.ts deleted file mode 100644 index a76c35a..0000000 --- a/packages/core/tests/unit/SingerSearchService.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; -import type { SingerDetailsResponseDto } from "../../src/modules"; -import type { IRepositoryWithGetDetails } from "../../src/types/repository"; - -const mockGetLocalizedIndexesOfEntity = mock(); -const mockGetAdminIndex = mock(); -const mockGetSearchIndex = mock(); - -const mockAdminIndex = { - deleteDocument: mock(), - addDocuments: mock(), - search: mock(), -}; - -const mockSearchIndex = { - search: mock(), -}; - -const mockSearchManager = { - getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, - getAdminIndex: mockGetAdminIndex, - getSearchIndex: mockGetSearchIndex, - waitForTask: mock().mockResolvedValue(undefined), -}; - -const mockEmbeddingsPost = mock(); -const mockEmbeddingManager = { - embeddings: { - post: mockEmbeddingsPost, - }, -}; - -const { SingerSearchService } = await import("../../src/search/catalog/singer"); - -const mockSingerDetails: SingerDetailsResponseDto = { - id: 1, - name: "Test Singer", - language: "zh", - avatarUrl: null, - description: "A test singer", - localizedNames: { en: "Test Singer EN", ja: "テストシンガー" }, - localizedDescriptions: { en: "A test singer in English" }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; - -describe("SingerSearchService", () => { - let service: InstanceType; - let mockRepository: IRepositoryWithGetDetails; - - beforeEach(() => { - mockGetLocalizedIndexesOfEntity.mockClear(); - mockGetAdminIndex.mockClear(); - mockGetSearchIndex.mockClear(); - mockAdminIndex.deleteDocument.mockClear(); - mockAdminIndex.addDocuments.mockClear(); - mockAdminIndex.search.mockClear(); - mockSearchIndex.search.mockClear(); - mockEmbeddingsPost.mockClear(); - - mockGetAdminIndex.mockResolvedValue(mockAdminIndex); - mockGetSearchIndex.mockResolvedValue(mockSearchIndex); - mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); - mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); - mockEmbeddingsPost.mockResolvedValue({ - data: { embeddings: [[0.1, 0.2, 0.3]] }, - }); - - mockRepository = { - getDetailsById: mock(), - }; - - service = new SingerSearchService( - mockRepository, - mockSearchManager as never, - mockEmbeddingManager as never - ); - }); - - afterAll(() => { - mock.restore(); - mock.clearAllMocks(); - }); - - describe("sync", () => { - test("syncs singer to all language indexes", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSingerDetails - ); - - await service.sync(1); - - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0] as unknown[]; - expect(callArgs[1]).toEqual({ primaryKey: "id" }); - }); - - test("deletes document from all indexes when singer not found", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); - mockGetLocalizedIndexesOfEntity.mockResolvedValue(["singer_zh", "singer_en"]); - - await service.sync(999); - - expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("singer"); - expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); - }); - - test("handles missing search manager gracefully", async () => { - const serviceWithoutManager = new SingerSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSingerDetails - ); - - await serviceWithoutManager.sync(1); - - expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); - }); - - test("builds document with localized content", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSingerDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const allCalls = mockAdminIndex.addDocuments.mock.calls as unknown as [ - { id: number; name: string }[], - unknown, - ][]; - const enCall = allCalls.find((call) => call[0][0].name === "Test Singer EN"); - expect(enCall).toBeDefined(); - }); - - test("handles singer with no localized content", async () => { - const singerWithoutLocalization = { - ...mockSingerDetails, - localizedNames: {}, - localizedDescriptions: {}, - }; - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - singerWithoutLocalization - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - }); - - test("handles embedding generation failure", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSingerDetails - ); - mockEmbeddingsPost.mockResolvedValue({ - data: null, - }); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const doc = ( - (mockAdminIndex.addDocuments.mock.calls[0] as unknown[])[0] as { - _vectors: { "potion-multilingual-128M": unknown }; - }[] - )[0]; - expect(doc._vectors).toEqual({ - "potion-multilingual-128M": null, - }); - }); - }); - - describe("search", () => { - test("performs hybrid search with embedding", async () => { - const mockSearchResult = { - hits: [{ id: 1, name: "Test Singer" }], - query: "test", - processingTimeMs: 30, - offset: 1, - limit: 1, - estimatedTotalHits: 1, - }; - mockSearchIndex.search.mockResolvedValue(mockSearchResult); - - const result = await service.search("test query", "zh"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("singer_zh"); - expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); - expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { - vector: [0.1, 0.2, 0.3], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, - showRankingScore: true, - }); - expect(result).toEqual(mockSearchResult); - }); - - test("uses default language when not specified", async () => { - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("singer_zh"); - }); - - test("throws when search manager not available", async () => { - const serviceWithoutManager = new SingerSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - expect(serviceWithoutManager.search("test")).rejects.toThrow( - "Search or embedding service not available" - ); - }); - - test("handles missing embedding response gracefully", async () => { - mockEmbeddingsPost.mockResolvedValue({ data: null }); - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test", "zh"); - - expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { - vector: undefined, - hybrid: undefined, - showRankingScore: true, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/SingerService.test.ts b/packages/core/tests/unit/SingerService.test.ts deleted file mode 100644 index 437e267..0000000 --- a/packages/core/tests/unit/SingerService.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { prisma } from "@cvsa/db"; -import { AppError } from "../../src/error/AppError"; -import { SingerService } from "../../src/modules/catalog/singer/service"; -import type { SingerDetailsResponseDto } from "../../src/modules/catalog/singer/dto"; -import type { ISingerRepository } from "../../src/modules/catalog/singer/repository.interface"; -import type { OutboxService } from "../../src/modules/outbox/service"; -import { createMockRepository } from "../utils"; - -( - prisma as unknown as { - $transaction: (fn: (tx: unknown) => Promise) => Promise; - } -).$transaction = mock(async (fn: (tx: unknown) => Promise) => fn(prisma)); - -const mockSingerDetails: SingerDetailsResponseDto = { - id: 1, - name: "Test Singer", - avatarUrl: null, - language: "zh", - localizedNames: null, - description: "A test singer", - localizedDescriptions: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; - -const mockOutboxEntry = { - id: 1, - aggregateType: "singer" as const, - aggregateId: 1, - eventType: "singer.created" as const, - payload: null, - status: "PENDING" as const, - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, -}; - -describe("SingerService", () => { - const mockRepository = createMockRepository({ - getById: async (id: number) => { - if (id === 1) { - return mockSingerDetails; - } - return null; - }, - getDetailsById: async (id: number) => { - if (id === 1) { - return mockSingerDetails; - } - return null; - }, - create: async () => mockSingerDetails, - update: async () => mockSingerDetails, - softDelete: async () => {}, - }); - - const mockOutboxService = { - createEntry: mock(async () => mockOutboxEntry), - enqueue: mock(async () => {}), - processEntry: mock(async () => {}), - recoverStaleEntries: mock(async () => {}), - }; - - const singerService = new SingerService( - mockRepository as unknown as ISingerRepository, - mockOutboxService as unknown as OutboxService - ); - - describe("getDetails", () => { - test("returns singer details when singer exists", async () => { - const result = await singerService.getDetails(1); - - expect(result).toEqual(mockSingerDetails); - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when singer does not exist", async () => { - mockRepository.getDetailsById.mockResolvedValueOnce(null); - - expect(singerService.getDetails(999)).rejects.toThrow(AppError); - expect(singerService.getDetails(999)).rejects.toThrow("error.singer.notfound"); - expect(singerService.getDetails(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("create", () => { - const createInput = { - name: "New Singer", - language: "ja", - }; - - test("creates singer and calls outbox.createEntry and outbox.enqueue", async () => { - const result = await singerService.create(createInput); - - expect(result).toMatchObject({ - name: "Test Singer", - language: "zh", - }); - expect(mockRepository.create).toHaveBeenCalledWith(createInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "singer", - aggregateId: mockSingerDetails.id, - eventType: "singer.created", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - }); - - describe("update", () => { - const updateInput = { name: "Updated Singer" }; - - test("updates singer and calls outbox.createEntry and outbox.enqueue on success", async () => { - const result = await singerService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Test Singer", - language: "zh", - }); - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalledWith(1, updateInput, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "singer", - aggregateId: 1, - eventType: "singer.updated", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when singer does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(singerService.update(999, updateInput)).rejects.toThrow(AppError); - expect(singerService.update(999, updateInput)).rejects.toThrow("error.singer.notfound"); - expect(singerService.update(999, updateInput)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("delete", () => { - test("soft deletes singer and calls outbox.createEntry and outbox.enqueue on success", async () => { - await singerService.delete(1); - - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalledWith(1, expect.anything()); - expect(mockOutboxService.createEntry).toHaveBeenCalledWith( - { - aggregateType: "singer", - aggregateId: 1, - eventType: "singer.deleted", - }, - expect.anything() - ); - expect(mockOutboxService.enqueue).toHaveBeenCalledWith(mockOutboxEntry); - }); - - test("throws NOT_FOUND error when singer does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(singerService.delete(999)).rejects.toThrow(AppError); - expect(singerService.delete(999)).rejects.toThrow("error.singer.notfound"); - expect(singerService.delete(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); -}); diff --git a/packages/core/tests/unit/SongSearchService.test.ts b/packages/core/tests/unit/SongSearchService.test.ts deleted file mode 100644 index b5e585a..0000000 --- a/packages/core/tests/unit/SongSearchService.test.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { describe, expect, mock, test, beforeEach, afterAll } from "bun:test"; -import type { SongDetailsResponseDto } from "../../src/modules"; -import type { IRepositoryWithGetDetails } from "../../src/types/repository"; - -// Mock SearchManager -const mockGetLocalizedIndexesOfEntity = mock(); -const mockGetAdminIndex = mock(); -const mockGetSearchIndex = mock(); - -const mockAdminIndex = { - deleteDocument: mock(), - addDocuments: mock(), - search: mock(), -}; - -const mockSearchIndex = { - search: mock(), -}; - -const mockSearchManager = { - getLocalizedIndexesOfEntity: mockGetLocalizedIndexesOfEntity, - getAdminIndex: mockGetAdminIndex, - getSearchIndex: mockGetSearchIndex, - waitForTask: mock().mockResolvedValue(undefined), -}; - -// Mock EmbeddingAppApi -const mockEmbeddingsPost = mock(); -const mockEmbeddingManager = { - embeddings: { - post: mockEmbeddingsPost, - }, -}; - -// Import after mocking -const { SongSearchService } = await import("../../src/search/catalog/song"); - -const mockSongDetails: SongDetailsResponseDto = { - id: 1, - type: "ORIGINAL", - name: "Test Song", - language: "zh", - duration: 180, - description: "A test song", - coverUrl: "https://example.com/cover.jpg", - publishedAt: new Date("2024-01-01").toISOString(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - originalSongId: null, - localizedNames: { en: "Test Song EN", ja: "テストソング" }, - localizedDescriptions: { en: "A test song in English" }, - bilibiliAid: null, - bilibiliBvid: null, - vocadbId: null, - vcpediaId: null, - moegirlId: null, - singers: [ - { - id: 1, - name: "Singer 1", - language: "zh", - localizedNames: { en: "Singer 1 EN" }, - engine: "SynthV", - description: null, - localizedDescriptions: null, - createdAt: "", - updatedAt: "", - avatarUrl: null, - }, - ], - artists: [ - { - id: 1, - name: "Artist 1", - language: "zh", - localizedNames: { en: "Artist 1 EN" }, - description: null, - localizedDescriptions: null, - createdAt: "", - updatedAt: "", - aliases: [], - userId: null, - role: { - language: "zh", - id: 1, - name: "", - localizedNames: null, - createdAt: "", - updatedAt: "", - deletedAt: null, - }, - }, - ], - lyrics: [ - { - language: "zh", - plainText: "歌词内容", - isTranslated: false, - ttml: "", - lrc: "", - id: 1, - createdAt: "", - updatedAt: "", - songId: 1, - }, - { - language: "en", - plainText: "Lyrics content", - isTranslated: true, - ttml: "", - lrc: "", - id: 1, - createdAt: "", - updatedAt: "", - songId: 1, - }, - ], -}; - -describe("SongSearchService", () => { - let service: InstanceType; - let mockRepository: IRepositoryWithGetDetails; - - beforeEach(() => { - mockGetLocalizedIndexesOfEntity.mockClear(); - mockGetAdminIndex.mockClear(); - mockGetSearchIndex.mockClear(); - mockAdminIndex.deleteDocument.mockClear(); - mockAdminIndex.addDocuments.mockClear(); - mockAdminIndex.search.mockClear(); - mockSearchIndex.search.mockClear(); - mockEmbeddingsPost.mockClear(); - - mockGetAdminIndex.mockResolvedValue(mockAdminIndex); - mockGetSearchIndex.mockResolvedValue(mockSearchIndex); - mockAdminIndex.deleteDocument.mockResolvedValue({ taskUid: 1 }); - mockAdminIndex.addDocuments.mockResolvedValue({ taskUid: 1 }); - mockEmbeddingsPost.mockResolvedValue({ - data: { embeddings: [[0.1, 0.2, 0.3]] }, - }); - - mockRepository = { - getDetailsById: mock(), - }; - - service = new SongSearchService( - mockRepository, - mockSearchManager as never, - mockEmbeddingManager as never - ); - }); - - afterAll(() => { - mock.restore(); - mock.clearAllMocks(); - }); - - describe("sync", () => { - test("syncs song to all language indexes", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - expect(callArgs[1]).toEqual({ primaryKey: "id" }); - }); - - test("deletes document from all indexes when song not found", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue(null); - mockGetLocalizedIndexesOfEntity.mockResolvedValue(["song_zh", "song_en"]); - - await service.sync(999); - - expect(mockGetLocalizedIndexesOfEntity).toHaveBeenCalledWith("song"); - expect(mockGetAdminIndex).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledTimes(2); - expect(mockAdminIndex.deleteDocument).toHaveBeenCalledWith(999); - }); - - test("handles missing search manager gracefully", async () => { - const serviceWithoutManager = new SongSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await serviceWithoutManager.sync(1); - - expect(mockRepository.getDetailsById).not.toHaveBeenCalled(); - }); - - test("builds document with localized content", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - expect(callArgs).toBeDefined(); - const docs = callArgs[0] as { id: number; name: string }[]; - expect(docs[0].id).toBe(1); - expect(docs[0].name).toBe("Test Song EN"); - }); - - test("handles song with no localized content", async () => { - const songWithoutLocalization = { - ...mockSongDetails, - localizedNames: {}, - localizedDescriptions: {}, - singers: [{ ...mockSongDetails.singers[0], localizedNames: {} }], - artists: [{ ...mockSongDetails.artists[0], localizedNames: {} }], - }; - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - songWithoutLocalization - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - }); - - test("handles embedding generation failure", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - mockEmbeddingsPost.mockResolvedValue({ - data: null, - }); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const doc = mockAdminIndex.addDocuments.mock.calls[0][0][0]; - expect(doc._vectors).toEqual({ - "potion-multilingual-128M": null, - }); - }); - }); - - describe("search", () => { - test("performs hybrid search with embedding", async () => { - const mockSearchResult = { - hits: [{ id: 1, name: "Test Song" }], - query: "test", - processingTimeMs: 30, - offset: 1, - limit: 1, - estimatedTotalHits: 1, - }; - mockSearchIndex.search.mockResolvedValue(mockSearchResult); - - const result = await service.search("test query", "zh"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("song_zh"); - expect(mockEmbeddingsPost).toHaveBeenCalledWith({ texts: ["test query"] }); - expect(mockSearchIndex.search).toHaveBeenCalledWith("test query", { - vector: [0.1, 0.2, 0.3], - hybrid: { - embedder: "potion-multilingual-128M", - semanticRatio: 0.25, - }, - showRankingScore: true, - }); - expect(result).toEqual(mockSearchResult); - }); - - test("uses default language when not specified", async () => { - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test"); - - expect(mockGetSearchIndex).toHaveBeenCalledWith("song_zh"); - }); - - test("throws when search manager not available", async () => { - const serviceWithoutManager = new SongSearchService( - mockRepository, - undefined as never, - mockEmbeddingManager as never - ); - - expect(serviceWithoutManager.search("test")).rejects.toThrow( - "Search or embedding service not available" - ); - }); - - test("handles missing embedding response gracefully", async () => { - mockEmbeddingsPost.mockResolvedValue({ data: null }); - mockSearchIndex.search.mockResolvedValue({ hits: [] }); - - await service.search("test", "zh"); - - expect(mockSearchIndex.search).toHaveBeenCalledWith("test", { - vector: undefined, - hybrid: undefined, - showRankingScore: true, - }); - }); - }); - - describe("getDocument (via sync)", () => { - test("uses correct lyrics for language", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - expect(callArgs).toBeDefined(); - const docs = callArgs[0] as { lyrics?: string }[]; - expect(docs[0].lyrics).toBeDefined(); - }); - - test("uses correct description for language", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const allCalls = mockAdminIndex.addDocuments.mock.calls; - const zhCall = allCalls.find((call: unknown[]) => { - const docs = call[0] as { description?: string }[]; - return docs[0].description === "A test song"; - }); - expect(zhCall).toBeDefined(); - }); - - test("uses localized description when available", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callsCount = mockAdminIndex.addDocuments.mock.calls.length; - expect(callsCount).toBeGreaterThan(0); - }); - - test("extracts localized singer names", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - const docs = callArgs[0] as { singers?: string[] }[]; - expect(docs[0].singers).toBeDefined(); - }); - - test("extracts localized artist names", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - const docs = callArgs[0] as { artists?: string[] }[]; - expect(docs[0].artists).toBeDefined(); - }); - - test("extracts engine from singers", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - const docs = callArgs[0] as { engine?: string[] }[]; - expect(docs[0].engine).toContain("SynthV"); - }); - - test("converts publishedAt to timestamp", async () => { - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - mockSongDetails - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - const docs = callArgs[0] as { publishedAt?: number }[]; - expect(docs[0].publishedAt).toBeTypeOf("number"); - expect(docs[0].publishedAt).toBe(new Date("2024-01-01").getTime()); - }); - - test("handles null publishedAt", async () => { - const songWithoutDate = { - ...mockSongDetails, - publishedAt: null, - }; - (mockRepository.getDetailsById as ReturnType).mockResolvedValue( - songWithoutDate - ); - - await service.sync(1); - - expect(mockAdminIndex.addDocuments).toHaveBeenCalled(); - const callArgs = mockAdminIndex.addDocuments.mock.calls[0]; - const docs = callArgs[0] as { publishedAt?: number }[]; - expect(docs[0].publishedAt).toBeUndefined(); - }); - }); -}); diff --git a/packages/core/tests/unit/SongService.test.ts b/packages/core/tests/unit/SongService.test.ts deleted file mode 100644 index 4a1529a..0000000 --- a/packages/core/tests/unit/SongService.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { SongService, AppError } from "@cvsa/core/internal"; -import type { SongDetailsResponseDto, ISongRepository, OutboxService } from "@cvsa/core"; -import { createMockRepository } from "../utils"; - -const mockSongDetails: SongDetailsResponseDto = { - id: 1, - type: "ORIGINAL", - name: "Test Song", - language: "zh", - duration: 180, - description: "A test song", - coverUrl: "https://example.com/cover.jpg", - publishedAt: new Date("2024-01-01").toISOString(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - originalSongId: null, - localizedNames: {}, - localizedDescriptions: {}, - bilibiliAid: null, - bilibiliBvid: null, - vocadbId: null, - vcpediaId: null, - moegirlId: null, - singers: [], - artists: [], - lyrics: [], -}; - -describe("SongService", () => { - const mockRepository = createMockRepository({ - getById: async (id: number) => { - if (id === 1) { - return mockSongDetails; - } - return null; - }, - getDetailsById: async (id: number) => { - if (id === 1) { - return mockSongDetails; - } - return null; - }, - create: async () => mockSongDetails, - update: async () => mockSongDetails, - softDelete: async () => {}, - createLyrics: async () => ({ - id: 1, - language: "zh", - isTranslated: false, - plainText: "Test lyrics", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }), - getLyricsBySongId: async () => [], - getLyricById: async () => null, - updateLyric: async () => ({ - id: 1, - language: "zh", - isTranslated: false, - plainText: "Updated lyrics", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }), - softDeleteLyric: async () => {}, - }); - - const mockOutboxService = { - createEntry: mock(async () => ({ - id: 1, - aggregateType: "song", - aggregateId: 1, - eventType: "song.created", - payload: null, - status: "PENDING" as const, - retryCount: 0, - lastError: null, - nextRetryAt: null, - createdAt: new Date().toISOString(), - processedAt: null, - })), - enqueue: mock(async () => {}), - processEntry: mock(async () => {}), - recoverStaleEntries: mock(async () => {}), - }; - - const songService = new SongService( - mockRepository as unknown as ISongRepository, - mockOutboxService as unknown as OutboxService - ); - - describe("getDetails", () => { - test("returns song details when song exists", async () => { - const result = await songService.getDetails(1); - - expect(result).toEqual(mockSongDetails); - expect(mockRepository.getDetailsById).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getDetailsById.mockResolvedValue(null); - - expect(songService.getDetails(999)).rejects.toThrow(AppError); - expect(songService.getDetails(999)).rejects.toThrow("error.song.notfound"); - expect(songService.getDetails(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("create", () => { - const createInput = { - name: "New Song", - type: "ORIGINAL" as const, - duration: 200, - }; - - test("creates song and calls outbox.createEntry", async () => { - const result = await songService.create(createInput); - - expect(result).toMatchObject({ - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - expect(mockRepository.create).toHaveBeenCalled(); - }); - }); - - describe("update", () => { - const updateInput = { name: "Updated Song" }; - - test("updates song and calls outbox.createEntry on success", async () => { - const result = await songService.update(1, updateInput); - - expect(result).toMatchObject({ - name: "Test Song", - type: "ORIGINAL", - duration: 180, - }); - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.update).toHaveBeenCalled(); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.update(999, updateInput)).rejects.toThrow(AppError); - expect(songService.update(999, updateInput)).rejects.toThrow("error.song.notfound"); - expect(songService.update(999, updateInput)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("delete", () => { - test("soft deletes song and calls outbox.createEntry on success", async () => { - await songService.delete(1); - - expect(mockRepository.getById).toHaveBeenCalledWith(1); - expect(mockRepository.softDelete).toHaveBeenCalled(); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.delete(999)).rejects.toThrow(AppError); - expect(songService.delete(999)).rejects.toThrow("error.song.notfound"); - expect(songService.delete(999)).rejects.toMatchObject({ - code: "NOT_FOUND", - statusCode: 404, - }); - }); - }); - - describe("listLyrics", () => { - test("returns lyrics when song exists", async () => { - mockRepository.getLyricsBySongId.mockResolvedValueOnce([ - { - id: 1, - language: "zh", - isTranslated: false, - plainText: "歌词", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }, - ]); - - const result = await songService.listLyrics(1); - - expect(result).toHaveLength(1); - expect(result[0].language).toBe("zh"); - expect(mockRepository.getLyricsBySongId).toHaveBeenCalledWith(1); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.listLyrics(999)).rejects.toThrow(AppError); - expect(songService.listLyrics(999)).rejects.toThrow("error.song.notfound"); - }); - }); - - describe("getLyric", () => { - test("returns lyric when song and lyric exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce({ - id: 1, - language: "zh", - isTranslated: false, - plainText: "歌词", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); - - const result = await songService.getLyric(1, 1); - - expect(result.id).toBe(1); - expect(result.plainText).toBe("歌词"); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.getLyric(999, 1)).rejects.toThrow(AppError); - expect(songService.getLyric(999, 1)).rejects.toThrow("error.song.notfound"); - }); - - test("throws NOT_FOUND error when lyric does not exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce(null); - - expect(songService.getLyric(1, 999)).rejects.toThrow(AppError); - expect(songService.getLyric(1, 999)).rejects.toThrow("error.lyric.notfound"); - }); - }); - - describe("createLyric", () => { - const createInput = { - language: "ja" as const, - isTranslated: true, - plainText: "日本語の歌詞", - }; - - test("creates lyric when song exists", async () => { - const result = await songService.createLyric(1, createInput); - - expect(result).toMatchObject({ - language: "zh", - plainText: "Test lyrics", - }); - expect(mockRepository.createLyrics).toHaveBeenCalledWith( - 1, - createInput, - expect.anything() - ); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.createLyric(999, createInput)).rejects.toThrow(AppError); - expect(songService.createLyric(999, createInput)).rejects.toThrow( - "error.song.notfound" - ); - }); - }); - - describe("updateLyric", () => { - const updateInput = { plainText: "Updated lyrics" }; - - test("updates lyric when song and lyric exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce({ - id: 1, - language: "zh", - isTranslated: false, - plainText: "原歌词", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); - - const result = await songService.updateLyric(1, 1, updateInput); - - expect(result.plainText).toBe("Updated lyrics"); - expect(mockRepository.updateLyric).toHaveBeenCalledWith( - 1, - updateInput, - expect.anything() - ); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.updateLyric(999, 1, updateInput)).rejects.toThrow(AppError); - expect(songService.updateLyric(999, 1, updateInput)).rejects.toThrow( - "error.song.notfound" - ); - }); - - test("throws NOT_FOUND error when lyric does not exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce(null); - - expect(songService.updateLyric(1, 999, updateInput)).rejects.toThrow(AppError); - expect(songService.updateLyric(1, 999, updateInput)).rejects.toThrow( - "error.lyric.notfound" - ); - }); - }); - - describe("deleteLyric", () => { - test("soft deletes lyric when song and lyric exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce({ - id: 1, - language: "zh", - isTranslated: false, - plainText: "歌词", - ttml: null, - lrc: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); - - await songService.deleteLyric(1, 1); - - expect(mockRepository.softDeleteLyric).toHaveBeenCalledWith(1, expect.anything()); - }); - - test("throws NOT_FOUND error when song does not exist", async () => { - mockRepository.getById.mockResolvedValueOnce(null); - - expect(songService.deleteLyric(999, 1)).rejects.toThrow(AppError); - expect(songService.deleteLyric(999, 1)).rejects.toThrow("error.song.notfound"); - }); - - test("throws NOT_FOUND error when lyric does not exist", async () => { - mockRepository.getLyricById.mockResolvedValueOnce(null); - - expect(songService.deleteLyric(1, 999)).rejects.toThrow(AppError); - expect(songService.deleteLyric(1, 999)).rejects.toThrow("error.lyric.notfound"); - }); - }); -}); diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts deleted file mode 100644 index e94ca9a..0000000 --- a/packages/core/tests/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mock } from "bun:test"; - -export function createMockRepository(template: T) { - const mockRepo = {} as Record>; - - for (const key of Object.keys(template) as (keyof T)[]) { - if (typeof template[key] === "function") { - mockRepo[key] = mock(template[key] as never); - } - } - - return mockRepo; -} - -export const OriginalMeiliSearch = await import("meilisearch"); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index 9beb816..0000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["src/**/*.ts", "tests/**/*.ts"] -} diff --git a/packages/core/turbo.json b/packages/core/turbo.json deleted file mode 100644 index a576182..0000000 --- a/packages/core/turbo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "lint": { - "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] - }, - "test": { - "dependsOn": ["typecheck"], - "inputs": [ - "tests/**.ts", - "src/**.ts", - "$TURBO_ROOT$/packages/db/**/*.ts", - "$TURBO_ROOT$/packages/db/**/*.prisma" - ] - }, - "typecheck": { - "inputs": [ - "tests/**.ts", - "src/**.ts", - "tsconfig.json", - "$TURBO_ROOT$/packages/typescript-config/base.json" - ] - } - } -} diff --git a/packages/db/.env.example b/packages/db/.env.example index 89b66a8..701c7b7 100644 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1,5 +1 @@ -<<<<<<< HEAD -DATABASE_URL=postgres://username:password@localhost:5432/cvsa -======= -DATABASE_URL=postgres://cvsa:password@localhost:5432/cvsa ->>>>>>> origin/develop +DATABASE_URL=postgres://username:password@localhost:5432/cvsa \ No newline at end of file diff --git a/packages/db/README.md b/packages/db/README.md index 069ba9c..5c6b150 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -1,8 +1,4 @@ -<<<<<<< HEAD # @project-cvsa/db -======= -# @cvsa/db ->>>>>>> origin/develop ## Available Scripts diff --git a/packages/db/package.json b/packages/db/package.json index 0730e85..8c06bcc 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "name": "@project-cvsa/db", "type": "module", "module": "index.ts", @@ -24,34 +23,4 @@ "@prisma/client": "^7.4.1", "@types/pg": "^8.16.0" } -======= - "name": "@cvsa/db", - "type": "module", - "main": "src/index.ts", - "module": "src/index.ts", - "scripts": { - "gen": "bunx --bun prisma generate", - "migrate": "bunx --bun prisma migrate dev --create-only", - "migrate:dev": "bunx --bun prisma migrate dev", - "push": "bunx --bun prisma db push", - "db:deploy": "bunx --bun prisma migrate deploy", - "test:setup": "NODE_ENV=test bunx --bun prisma db push --accept-data-loss", - "test:reset": "NODE_ENV=test bunx --bun prisma db push --force-reset", - "studio": "bunx --bun prisma studio", - "reset": "bunx --bun prisma migrate reset" - }, - "devDependencies": { - "@cvsa/prisma-zod": "^3.6.0", - "@cvsa/typescript-config": "workspace:*", - "prisma": "^7.6.0", - "prisma-json-types-generator": "^4.1.1" - }, - "dependencies": { - "@cvsa/env": "workspace:*", - "@prisma/adapter-pg": "^7.6.0", - "@prisma/client": "^7.6.0", - "@types/pg": "^8.16.0", - "zod": "^4.3.6" - } ->>>>>>> origin/develop } diff --git a/packages/db/prisma/migrations/20260227101459_init/migration.sql b/packages/db/prisma/migrations/20260227101459_init/migration.sql new file mode 100644 index 0000000..e86dee7 --- /dev/null +++ b/packages/db/prisma/migrations/20260227101459_init/migration.sql @@ -0,0 +1,514 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "auth"; + +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "core"; + +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "meta"; + +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "platform"; + +-- CreateEnum +CREATE TYPE "core"."third_party_platform" AS ENUM ('YOUTUBE', 'NICONICO', 'BILIBILI', 'VOCALOID_WIKI', 'VOCALOID_LYRICS_WIKI', 'SOUNDCLOUD', 'NETEASE_MUSIC', 'QQ_MUSIC', 'FIVE_SING', 'KUGOU', 'SPOTIFY', 'APPLE_MUSIC', 'YOUTUBE_MUSIC', 'WIKIPEDIA', 'BAIDU_BAIKE', 'PIXIV', 'WEIBO', 'TWITTER', 'VOCADB', 'MOEGIRLPEDIA', 'MUSICBRAINZ', 'VCPEDIA', 'XIAOHONGSHU'); + +-- CreateEnum +CREATE TYPE "core"."song_type" AS ENUM ('ORIGINAL', 'COVER', 'REMIX', 'REMASTER', 'MASHUP', 'INSTRUMENTAL', 'OTHERS'); + +-- CreateEnum +CREATE TYPE "meta"."change_type" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'RESTORE'); + +-- CreateTable +CREATE TABLE "auth"."session" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "ip_address" TEXT, + "user_agent" TEXT, + "secret_hash" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "last_verified_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth"."user" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "display_name" TEXT, + "email" TEXT, + "password" TEXT NOT NULL, + "role_id" INTEGER, + "reputation" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."album" ( + "id" SERIAL NOT NULL, + "name" TEXT, + "localized_names" JSONB, + "description" TEXT, + "localized_descriptions" JSONB, + "cover_url" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "album_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."artist" ( + "id" SERIAL NOT NULL, + "name" TEXT, + "localized_names" JSONB, + "aliases" TEXT[], + "description" TEXT, + "localized_descriptions" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "user_id" TEXT, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "artist_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."artist_role" ( + "id" SERIAL NOT NULL, + "role" TEXT NOT NULL, + "localized_roles" JSONB, + "song_id" INTEGER NOT NULL, + "artist_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "artist_role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."external_link" ( + "id" SERIAL NOT NULL, + "label" TEXT, + "url" TEXT NOT NULL, + "platform" "core"."third_party_platform", + "platform_id" TEXT, + "song_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "external_link_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."group" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "group_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."lyrics" ( + "id" SERIAL NOT NULL, + "song_id" INTEGER NOT NULL, + "language" TEXT, + "plain_text" TEXT, + "ttml" TEXT, + "lrc" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "lyrics_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."singer" ( + "id" SERIAL NOT NULL, + "name" TEXT, + "avatar_url" TEXT, + "localized_names" JSONB, + "description" TEXT, + "localized_descriptions" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "singer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."song" ( + "id" SERIAL NOT NULL, + "type" "core"."song_type", + "original_song_id" INTEGER, + "name" TEXT, + "duration" INTEGER, + "localized_names" JSONB, + "description" TEXT, + "localized_descriptions" JSONB, + "bilibili_aid" BIGINT, + "bilibili_bvid" TEXT, + "vocadb_id" INTEGER, + "vcpedia_id" INTEGER, + "moegirl_id" INTEGER, + "cover_url" TEXT, + "published_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "song_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."song_series" ( + "id" SERIAL NOT NULL, + "name" TEXT, + "localized_names" JSONB, + "description" TEXT, + "localized_descriptions" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "song_series_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."svs_engine" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "localized_descriptions" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "svs_engine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."svs_engine_version" ( + "id" SERIAL NOT NULL, + "version_string" TEXT NOT NULL, + "svs_engine_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "svs_engine_version_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."tag" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "localized_names" JSONB, + "parent_id" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."voicebank" ( + "id" SERIAL NOT NULL, + "description" TEXT, + "localized_descriptions" JSONB, + "singer_id" INTEGER NOT NULL, + "language" TEXT NOT NULL, + "svs_engine_version_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "voicebank_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "meta"."history" ( + "id" SERIAL NOT NULL, + "user_id" TEXT NOT NULL, + "table" TEXT NOT NULL, + "object_id" INTEGER NOT NULL, + "type" "meta"."change_type" NOT NULL, + "comment" TEXT, + "old" JSONB, + "new" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "history_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "meta"."permission" ( + "id" SERIAL NOT NULL, + "action" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "meta"."role" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "localized_names" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "platform"."file" ( + "id" TEXT NOT NULL, + "original_name" TEXT, + "path" TEXT, + "user_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "file_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "platform"."post" ( + "id" TEXT NOT NULL, + "text" TEXT, + "user_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "platform"."reputation_history" ( + "id" SERIAL NOT NULL, + "user_id" TEXT NOT NULL, + "action_type" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "source_type" INTEGER NOT NULL, + "source_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "reputation_history_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."singer_of_song" ( + "id" SERIAL NOT NULL, + "song_id" INTEGER NOT NULL, + "singer_id" INTEGER NOT NULL, + "voicebank_id" INTEGER, + "svs_engine_id" INTEGER, + "svs_engine_version_id" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "singer_of_song_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."singer_svs_engine" ( + "id" SERIAL NOT NULL, + "singer_id" INTEGER NOT NULL, + "svs_engine_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "singer_svs_engine_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."singer_svs_engine_version" ( + "id" SERIAL NOT NULL, + "singer_id" INTEGER NOT NULL, + "svs_engine_version_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "singer_svs_engine_version_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."song_in_album" ( + "id" SERIAL NOT NULL, + "song_id" INTEGER NOT NULL, + "album_id" INTEGER NOT NULL, + "track_number" INTEGER NOT NULL, + "disc_number" INTEGER NOT NULL DEFAULT 1, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "song_in_album_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."song_in_series" ( + "id" SERIAL NOT NULL, + "song_id" INTEGER NOT NULL, + "series_id" INTEGER NOT NULL, + "order" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "song_in_series_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "core"."song_tag" ( + "id" SERIAL NOT NULL, + "song_id" INTEGER NOT NULL, + "tag_id" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "song_tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "meta"."role_permission" ( + "id" SERIAL NOT NULL, + "role_id" INTEGER NOT NULL, + "permission_id" INTEGER NOT NULL, + + CONSTRAINT "role_permission_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_username_key" ON "auth"."user"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "auth"."user"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "artist_user_id_key" ON "core"."artist"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "role_name_key" ON "meta"."role"("name"); + +-- AddForeignKey +ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth"."user" ADD CONSTRAINT "user_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "meta"."role"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."artist" ADD CONSTRAINT "artist_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."artist_role" ADD CONSTRAINT "artist_role_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."artist_role" ADD CONSTRAINT "artist_role_artist_id_fkey" FOREIGN KEY ("artist_id") REFERENCES "core"."artist"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."external_link" ADD CONSTRAINT "external_link_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."lyrics" ADD CONSTRAINT "lyrics_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song" ADD CONSTRAINT "song_original_song_id_fkey" FOREIGN KEY ("original_song_id") REFERENCES "core"."song"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."svs_engine_version" ADD CONSTRAINT "svs_engine_version_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."tag" ADD CONSTRAINT "tag_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "core"."tag"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."voicebank" ADD CONSTRAINT "voicebank_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."voicebank" ADD CONSTRAINT "voicebank_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "meta"."history" ADD CONSTRAINT "history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "platform"."file" ADD CONSTRAINT "file_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "platform"."post" ADD CONSTRAINT "post_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "platform"."reputation_history" ADD CONSTRAINT "reputation_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_voicebank_id_fkey" FOREIGN KEY ("voicebank_id") REFERENCES "core"."voicebank"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_of_song" ADD CONSTRAINT "singer_of_song_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_svs_engine" ADD CONSTRAINT "singer_svs_engine_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_svs_engine" ADD CONSTRAINT "singer_svs_engine_svs_engine_id_fkey" FOREIGN KEY ("svs_engine_id") REFERENCES "core"."svs_engine"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_svs_engine_version" ADD CONSTRAINT "singer_svs_engine_version_singer_id_fkey" FOREIGN KEY ("singer_id") REFERENCES "core"."singer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."singer_svs_engine_version" ADD CONSTRAINT "singer_svs_engine_version_svs_engine_version_id_fkey" FOREIGN KEY ("svs_engine_version_id") REFERENCES "core"."svs_engine_version"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_in_album" ADD CONSTRAINT "song_in_album_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_in_album" ADD CONSTRAINT "song_in_album_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "core"."album"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_in_series" ADD CONSTRAINT "song_in_series_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_in_series" ADD CONSTRAINT "song_in_series_series_id_fkey" FOREIGN KEY ("series_id") REFERENCES "core"."song_series"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_tag" ADD CONSTRAINT "song_tag_song_id_fkey" FOREIGN KEY ("song_id") REFERENCES "core"."song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "core"."song_tag" ADD CONSTRAINT "song_tag_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "core"."tag"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "meta"."role_permission" ADD CONSTRAINT "role_permission_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "meta"."role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "meta"."role_permission" ADD CONSTRAINT "role_permission_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "meta"."permission"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/packages/db/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/packages/db/prisma/models/auth/account.prisma b/packages/db/prisma/models/auth/account.prisma deleted file mode 100644 index 4aea913..0000000 --- a/packages/db/prisma/models/auth/account.prisma +++ /dev/null @@ -1,20 +0,0 @@ -model Account { - id String @id - accountId String - providerId String - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - accessToken String? - refreshToken String? - idToken String? - accessTokenExpiresAt DateTime? - refreshTokenExpiresAt DateTime? - scope String? - password String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@map("account") - @@schema("auth") -} diff --git a/packages/db/prisma/models/auth/session.prisma b/packages/db/prisma/models/auth/session.prisma index 6e137ba..bca0256 100644 --- a/packages/db/prisma/models/auth/session.prisma +++ b/packages/db/prisma/models/auth/session.prisma @@ -1,5 +1,4 @@ model Session { -<<<<<<< HEAD id String @id user User @relation(fields: [userId], references: [id]) userId String @map("user_id") @@ -9,20 +8,6 @@ model Session { createdAt DateTime @default(now()) @map("created_at") lastVerifiedAt DateTime @default(now()) @map("last_verified_at") -======= - id String @id - token String - user User @relation(fields: [userId], references: [id]) - userId String @map("user_id") - ipAddress String? @map("ip_address") - userAgent String? @map("user_agent") - expiresAt DateTime - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@unique([token]) - @@index([userId]) ->>>>>>> origin/develop @@map("session") @@schema("auth") } diff --git a/packages/db/prisma/models/auth/user.prisma b/packages/db/prisma/models/auth/user.prisma index 03147d1..f3260cc 100644 --- a/packages/db/prisma/models/auth/user.prisma +++ b/packages/db/prisma/models/auth/user.prisma @@ -1,23 +1,13 @@ model User { id String @id -<<<<<<< HEAD username String @unique displayName String? @map("display_name") email String? @unique password String -======= - name String? - username String @unique - displayUsername String @unique @map("display_username") - email String? @unique - emailVerified Boolean @default(false) @map("email_verified") - image String? ->>>>>>> origin/develop changesMade History[] role Role? @relation(fields: [roleId], references: [id]) roleId Int? @map("role_id") sessions Session[] - accounts Account[] artist Artist? reputation Int @default(0) reputationHistories ReputationHistory[] @@ -25,6 +15,7 @@ model User { files File[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("user") @@schema("auth") diff --git a/packages/db/prisma/models/auth/verification.prisma b/packages/db/prisma/models/auth/verification.prisma deleted file mode 100644 index a465808..0000000 --- a/packages/db/prisma/models/auth/verification.prisma +++ /dev/null @@ -1,12 +0,0 @@ -model Verification { - id String @id - identifier String - value String - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([identifier]) - @@map("verification") - @@schema("auth") -} diff --git a/packages/db/prisma/models/core/album.prisma b/packages/db/prisma/models/core/album.prisma index c850552..2c22677 100644 --- a/packages/db/prisma/models/core/album.prisma +++ b/packages/db/prisma/models/core/album.prisma @@ -6,7 +6,6 @@ model Album { description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") - language String @default("zh") songInAlbums SongInAlbum[] coverUrl String? @map("cover_url") createdAt DateTime @default(now()) @map("created_at") diff --git a/packages/db/prisma/models/core/artist.prisma b/packages/db/prisma/models/core/artist.prisma index 53efd44..0debcae 100644 --- a/packages/db/prisma/models/core/artist.prisma +++ b/packages/db/prisma/models/core/artist.prisma @@ -1,5 +1,4 @@ model Artist { -<<<<<<< HEAD id Int @id @default(autoincrement()) name String? /// [LocalizedField] @@ -14,23 +13,6 @@ model Artist { user User? @relation(fields: [userId], references: [id]) userId String? @unique @map("user_id") deletedAt DateTime? @map("deleted_at") -======= - id Int @id @default(autoincrement()) - name String? - /// [LocalizedField] - localizedNames Json? @map("localized_names") - language String @default("zh") - aliases String[] - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - creations Creation[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User? @relation(fields: [userId], references: [id]) - userId String? @unique @map("user_id") - deletedAt DateTime? @map("deleted_at") ->>>>>>> origin/develop @@map("artist") @@schema("core") diff --git a/packages/db/prisma/models/core/artistRole.prisma b/packages/db/prisma/models/core/artistRole.prisma index 48c746b..40dc72b 100644 --- a/packages/db/prisma/models/core/artistRole.prisma +++ b/packages/db/prisma/models/core/artistRole.prisma @@ -1,5 +1,4 @@ model ArtistRole { -<<<<<<< HEAD id Int @id @default(autoincrement()) role String /// [LocalizedField] @@ -11,17 +10,6 @@ model ArtistRole { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") -======= - id Int @id @default(autoincrement()) - name String - /// [LocalizedField] - localizedNames Json? @map("localized_names") - language String @default("zh") - creations Creation[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") ->>>>>>> origin/develop @@map("artist_role") @@schema("core") diff --git a/packages/db/prisma/models/core/lyrics.prisma b/packages/db/prisma/models/core/lyrics.prisma index bdc2119..7ee4257 100644 --- a/packages/db/prisma/models/core/lyrics.prisma +++ b/packages/db/prisma/models/core/lyrics.prisma @@ -1,5 +1,4 @@ model Lyrics { -<<<<<<< HEAD id Int @id @default(autoincrement()) song Song @relation(fields: [songId], references: [id]) songId Int @map("song_id") @@ -10,19 +9,6 @@ model Lyrics { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") -======= - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - language String @default("zh") - isTranslated Boolean @default(false) @map("is_translated") - plainText String? @map("plain_text") - ttml String? - lrc String? - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") ->>>>>>> origin/develop @@map("lyrics") @@schema("core") diff --git a/packages/db/prisma/models/core/singer.prisma b/packages/db/prisma/models/core/singer.prisma index ba407ca..7cd3152 100644 --- a/packages/db/prisma/models/core/singer.prisma +++ b/packages/db/prisma/models/core/singer.prisma @@ -1,5 +1,4 @@ model Singer { -<<<<<<< HEAD id Int @id @default(autoincrement()) name String? avatarUrl String? @map("avatar_url") @@ -15,22 +14,6 @@ model Singer { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") -======= - id Int @id @default(autoincrement()) - name String? - avatarUrl String? @map("avatar_url") - language String @default("zh") - /// [LocalizedField] - localizedNames Json? @map("localized_names") - description String? - /// [LocalizedField] - localizedDescriptions Json? @map("localized_descriptions") - voicebanks Voicebank[] - performances Performance[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") ->>>>>>> origin/develop @@map("singer") @@schema("core") diff --git a/packages/db/prisma/models/core/song.prisma b/packages/db/prisma/models/core/song.prisma index f8b6c1a..48e6e62 100644 --- a/packages/db/prisma/models/core/song.prisma +++ b/packages/db/prisma/models/core/song.prisma @@ -4,13 +4,12 @@ model Song { originalVersion Song? @relation(name: "originalSong", fields: [originalSongId], references: [id]) originalSongId Int? @map("original_song_id") derivations Song[] @relation(name: "originalSong") - language String @default("zh") name String? - /// [LocalizedField] - localizedNames Json? @map("localized_names") duration Int? lyrics Lyrics[] songTags SongTag[] + /// [LocalizedField] + localizedNames Json? @map("localized_names") description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") @@ -22,13 +21,13 @@ model Song { moegirlId Int? @map("moegirl_id") externalLinks ExternalLink[] coverUrl String? @map("cover_url") - creations Creation[] - performances Performance[] - songInSeries SongInSeries[] + artists ArtistRole[] + singerOfSongs SingerOfSong[] publishedAt DateTime? @map("published_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") + songInSeries SongInSeries[] @@map("song") @@schema("core") @@ -37,9 +36,6 @@ model Song { enum SongType { ORIGINAL COVER - RETUNE - CONTRAFACTUM - TRANSLYRICS REMIX REMASTER MASHUP diff --git a/packages/db/prisma/models/core/songSeries.prisma b/packages/db/prisma/models/core/songSeries.prisma index ebc8896..414cadb 100644 --- a/packages/db/prisma/models/core/songSeries.prisma +++ b/packages/db/prisma/models/core/songSeries.prisma @@ -6,7 +6,6 @@ model SongSeries { description String? /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") - language String @default("zh") songInSeries SongInSeries[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/packages/db/prisma/models/core/svsEngine.prisma b/packages/db/prisma/models/core/svsEngine.prisma index 597d7aa..ac999f5 100644 --- a/packages/db/prisma/models/core/svsEngine.prisma +++ b/packages/db/prisma/models/core/svsEngine.prisma @@ -5,10 +5,11 @@ model SvsEngine { /// [LocalizedField] localizedDescriptions Json? @map("localized_descriptions") svsEngineVersions SvsEngineVersion[] + singerSvsEngines SingerSvsEngine[] + performances SingerOfSong[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - performances Performance[] @@map("svs_engine") @@schema("core") diff --git a/packages/db/prisma/models/core/svsEngineVersion.prisma b/packages/db/prisma/models/core/svsEngineVersion.prisma index fc4377a..154acd7 100644 --- a/packages/db/prisma/models/core/svsEngineVersion.prisma +++ b/packages/db/prisma/models/core/svsEngineVersion.prisma @@ -1,5 +1,4 @@ model SvsEngineVersion { -<<<<<<< HEAD id Int @id @default(autoincrement()) versionString String @map("version_string") singerSvsEngineVersions SingerSvsEngineVersion[] @@ -10,17 +9,6 @@ model SvsEngineVersion { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") -======= - id Int @id @default(autoincrement()) - versionString String @map("version_string") - svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int @map("svs_engine_id") - voicebanks Voicebank[] - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - performances Performance[] ->>>>>>> origin/develop @@map("svs_engine_version") @@schema("core") diff --git a/packages/db/prisma/models/core/voicebank.prisma b/packages/db/prisma/models/core/voicebank.prisma index 292e062..a7adae2 100644 --- a/packages/db/prisma/models/core/voicebank.prisma +++ b/packages/db/prisma/models/core/voicebank.prisma @@ -8,7 +8,7 @@ model Voicebank { language String svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) svsEngineVersionId Int @map("svs_engine_version_id") - performances Performance[] + singerOfSongs SingerOfSong[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") diff --git a/packages/db/prisma/models/meta/outbox.prisma b/packages/db/prisma/models/meta/outbox.prisma deleted file mode 100644 index 5416ad1..0000000 --- a/packages/db/prisma/models/meta/outbox.prisma +++ /dev/null @@ -1,27 +0,0 @@ -model Outbox { - id Int @id @default(autoincrement()) - aggregateType String @map("aggregate_type") - aggregateId Int @map("aggregate_id") - eventType String @map("event_type") - payload Json? - status OutboxStatus @default(PENDING) - retryCount Int @default(0) @map("retry_count") - lastError String? @map("last_error") - nextRetryAt DateTime? @map("next_retry_at") - createdAt DateTime @default(now()) @map("created_at") - processedAt DateTime? @map("processed_at") - - @@index([status, nextRetryAt]) - @@map("outbox") - @@schema("meta") -} - -enum OutboxStatus { - PENDING - PROCESSING - PROCESSED - FAILED - - @@map("outbox_status") - @@schema("meta") -} diff --git a/packages/db/prisma/relations/core/creation.prisma b/packages/db/prisma/relations/core/creation.prisma deleted file mode 100644 index 349dbae..0000000 --- a/packages/db/prisma/relations/core/creation.prisma +++ /dev/null @@ -1,15 +0,0 @@ -model Creation { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - artist Artist @relation(fields: [artistId], references: [id]) - artistId Int @map("artist_id") - role ArtistRole @relation(fields: [artistRoleId], references: [id]) - artistRoleId Int @map("artist_role_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("creation") - @@schema("core") -} diff --git a/packages/db/prisma/relations/core/performance.prisma b/packages/db/prisma/relations/core/performance.prisma deleted file mode 100644 index 113672e..0000000 --- a/packages/db/prisma/relations/core/performance.prisma +++ /dev/null @@ -1,22 +0,0 @@ -// Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.). -// In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese). -// That's why we need to have both SvsEngine and SvsEngineVersion. -model Performance { - id Int @id @default(autoincrement()) - song Song @relation(fields: [songId], references: [id]) - songId Int @map("song_id") - singer Singer @relation(fields: [singerId], references: [id]) - singerId Int @map("singer_id") - voicebank Voicebank? @relation(fields: [voicebankId], references: [id]) - voicebankId Int? @map("voicebank_id") - svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int? @map("svs_engine_id") - svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id]) - svsEngineVersionId Int? @map("svs_engine_version_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") - - @@map("performance") - @@schema("core") -} diff --git a/packages/db/prisma/relations/core/singerOfSong.prisma b/packages/db/prisma/relations/core/singerOfSong.prisma index 113672e..bd42ca5 100644 --- a/packages/db/prisma/relations/core/singerOfSong.prisma +++ b/packages/db/prisma/relations/core/singerOfSong.prisma @@ -1,7 +1,7 @@ // Sometimes, we only know the SVS engine used in the song (e.g. VOCALOID, Synthesizer V, UTAU, etc.). // In other cases, we know exactly which voicebank was used in the song. (e.g. Luo Tianyi VOCALOID 5 Chinese). // That's why we need to have both SvsEngine and SvsEngineVersion. -model Performance { +model SingerOfSong { id Int @id @default(autoincrement()) song Song @relation(fields: [songId], references: [id]) songId Int @map("song_id") @@ -10,13 +10,13 @@ model Performance { voicebank Voicebank? @relation(fields: [voicebankId], references: [id]) voicebankId Int? @map("voicebank_id") svsEngine SvsEngine? @relation(fields: [svsEngineId], references: [id]) - svsEngineId Int? @map("svs_engine_id") + svsEngineId Int? @map("svs_engine_id") svsEngineVersion SvsEngineVersion? @relation(fields: [svsEngineVersionId], references: [id]) svsEngineVersionId Int? @map("svs_engine_version_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") - @@map("performance") + @@map("singer_of_song") @@schema("core") } diff --git a/packages/db/prisma/relations/core/singerSvsEngine.prisma b/packages/db/prisma/relations/core/singerSvsEngine.prisma new file mode 100644 index 0000000..f4440b0 --- /dev/null +++ b/packages/db/prisma/relations/core/singerSvsEngine.prisma @@ -0,0 +1,13 @@ +model SingerSvsEngine { + id Int @id @default(autoincrement()) + singer Singer @relation(fields: [singerId], references: [id]) + singerId Int @map("singer_id") + svsEngine SvsEngine @relation(fields: [svsEngineId], references: [id]) + svsEngineId Int @map("svs_engine_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("singer_svs_engine") + @@schema("core") +} diff --git a/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma b/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma new file mode 100644 index 0000000..bc21e73 --- /dev/null +++ b/packages/db/prisma/relations/core/singerSvsEngineVersion.prisma @@ -0,0 +1,13 @@ +model SingerSvsEngineVersion { + id Int @id @default(autoincrement()) + singer Singer @relation(fields: [singerId], references: [id]) + singerId Int @map("singer_id") + svsEngineVersion SvsEngineVersion @relation(fields: [svsEngineVersionId], references: [id]) + svsEngineVersionId Int @map("svs_engine_version_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@map("singer_svs_engine_version") + @@schema("core") +} diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 06d7cad..34645f1 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -12,15 +12,7 @@ datasource db { schemas = ["core", "meta", "platform", "auth"] } -generator zod { - provider = "prisma-zod" - output = "./zod" - extendSchema = "zodSchema" - dateSchema = "string" - bigintSchema = "number" -} - -/* +/* # CVSA (Chinese Vocal Synthesis Archive) Database Schema ## Overview @@ -35,9 +27,9 @@ The database is partitioned into several PostgreSQL schemas. ### A. [core] - The Encyclopedia This is the heart of the project. It stores universal SVS data that remains objective across platforms. -- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), +- Entities: Songs, Singers (virtual characters), Artists (producers/illustrators), SVS Engines (Vocaloid, Synthesizer V), Voicebanks, Albums, and Tags. -- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata +- Localization: Uses `LocalizedField` (JSON) to support multi-language metadata (Simplified Chinese, Traditional Chinese, Japanese, English, etc.). - Versioning: Supports tracking of specific SVS engine versions and voicebank iterations. @@ -52,17 +44,17 @@ Handles user accounts, session persistence, and secure authentication. ### D. [meta] - Governance & Auditing Manages the "data about data." -- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) +- Audit Logs: The `History` table tracks every mutation (Create/Update/Delete) in the `core` schema for accountability and rollbacks. - Permissions: Granular action-based permissions assigned to roles. ## 3. Technical Constraints & Design Patterns - Soft Deletion: Most core entities implement a `deletedAt` timestamp for soft-deletion support. -- Modular Relations: Many-to-Many relations are extracted into separate files +- Modular Relations: Many-to-Many relations are extracted into separate files within the `/relations` directory to keep entity definitions clean and prevent Prisma file bloat. ## 4. Current Status & TODO - Indexing: Performance indexes and unique constraints beyond primary keys are still under design. - Constraints: Complex business logic constraints (e.g., preventing circular tag parents) are handled at the application layer. -*/ +*/ \ No newline at end of file diff --git a/packages/db/prisma/types.ts b/packages/db/prisma/types.ts index a205207..6121293 100644 --- a/packages/db/prisma/types.ts +++ b/packages/db/prisma/types.ts @@ -1,4 +1,3 @@ -<<<<<<< HEAD declare global { namespace PrismaJson { type LocalizedField = { @@ -9,13 +8,3 @@ declare global { // This file must be a module. export {}; -======= -import type { z } from "zod"; -import type { LocalizedField } from "./zodSchema"; - -declare global { - namespace PrismaJson { - type LocalizedField = z.infer; - } -} ->>>>>>> origin/develop diff --git a/packages/db/prisma/zodSchema.ts b/packages/db/prisma/zodSchema.ts deleted file mode 100644 index f4a0db2..0000000 --- a/packages/db/prisma/zodSchema.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { z } from "zod"; - -export const LocalizedField = z.record(z.string(), z.string()); diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts deleted file mode 100644 index b28453c..0000000 --- a/packages/db/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PrismaClient } from "./types"; - -export type TxClient = Omit< - PrismaClient, - "$transaction" | "$connect" | "$disconnect" | "$on" | "$use" | "$extends" ->; - -export * from "./prisma"; -export * from "./types"; diff --git a/packages/db/src/prisma.ts b/packages/db/src/prisma.ts deleted file mode 100644 index d6022a7..0000000 --- a/packages/db/src/prisma.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrismaClient } from "./types"; -import { PrismaPg } from "@prisma/adapter-pg"; -import { env } from "@cvsa/env"; - -const adapter = new PrismaPg({ - connectionString: env.DATABASE_URL, -}); - -/** - * Convert an type to a JSON-serializable version. - * It will convert `bigint` to `number`, - * and `Date` to `string` - */ -export type Serialized = T extends Date - ? string - : T extends bigint - ? number - : T extends Array - ? Array> - : T extends object - ? { [K in keyof T]: Serialized } - : T; - -export function transformPrismaResult(obj: T): Serialized { - if (obj instanceof Date) { - return obj.toISOString() as Serialized; - } - - if (typeof obj === "bigint") { - return Number(obj) as Serialized; - } - - if (Array.isArray(obj)) { - return obj.map((item) => transformPrismaResult(item)) as Serialized; - } - - if (typeof obj === "object" && obj !== null) { - const result: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - result[key] = transformPrismaResult(value); - } - - return result as Serialized; - } - - return obj as Serialized; -} - -export const prisma = new PrismaClient({ - adapter, - log: [ - { level: "query", emit: "event" }, - { level: "info", emit: "stdout" }, - { level: "warn", emit: "stdout" }, - { level: "error", emit: "stdout" }, - ], -}); diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts deleted file mode 100644 index a987e80..0000000 --- a/packages/db/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "../prisma/generated/client"; -export * from "../prisma/generated/models"; -export * from "../prisma/generated/enums"; -export * from "../prisma/zod"; -export * from "../prisma/types"; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json deleted file mode 100644 index 45730ba..0000000 --- a/packages/db/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["./src/*.ts"] -} diff --git a/packages/embedding/package.json b/packages/embedding/package.json deleted file mode 100644 index 9589927..0000000 --- a/packages/embedding/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@cvsa/embedding", - "type": "module", - "module": "src/index.ts", - "types": "src/index.ts", - "scripts": { - "start": "bun run src/index.ts", - "dev": "bun run --watch src/index.ts", - "test": "bun test" - }, - "dependencies": { - "@cvsa/logger": "workspace:*", - "@elysiajs/eden": "^1.4.9", - "@huggingface/hub": "^2.11.0", - "@huggingface/transformers": "^4.0.1", - "elysia": "latest", - "onnxruntime-node": "1.24.3", - "zod": "^4.3.6" - }, - "devDependencies": { - "bun-types": "latest", - "@cvsa/typescript-config": "workspace:*" - } -} diff --git a/packages/embedding/src/embedding.ts b/packages/embedding/src/embedding.ts deleted file mode 100644 index fbc38f8..0000000 --- a/packages/embedding/src/embedding.ts +++ /dev/null @@ -1,166 +0,0 @@ -import path from "node:path"; -import { AutoTokenizer, type PreTrainedTokenizer } from "@huggingface/transformers"; -import { fileDownloadInfo } from "@huggingface/hub"; -import * as ort from "onnxruntime-node"; -import { appLogger } from "@cvsa/logger"; - -const modelDir = path.join(import.meta.dir, "../../../model/"); - -// TODO: More formalized config -const modelName = "alikia2x/potion-multilingual-128M-int8-strip"; -const modelPath = path.join(modelDir, "./potion-strip/model.onnx"); - -export class EmbeddingManager { - private tokenizer: PreTrainedTokenizer | null = null; - private session: ort.InferenceSession | null = null; - - public async init(): Promise { - try { - await this.downloadModel(); - await this.initTokenizer(); - await this.initSession(); - return true; - } catch (e) { - appLogger.error(Bun.inspect(e)); - return false; - } - } - - private async initTokenizer(): Promise { - if (this.tokenizer !== null) { - return; - } - this.tokenizer = await AutoTokenizer.from_pretrained(modelName); - } - - private async initSession(): Promise { - if (this.session !== null) { - return; - } - this.session = await ort.InferenceSession.create(modelPath); - } - - private async getTokenizer(): Promise { - if (this.tokenizer === null) { - this.tokenizer = await AutoTokenizer.from_pretrained(modelName); - } - return this.tokenizer; - } - - private async getModelSession(): Promise { - if (this.session === null) { - this.session = await ort.InferenceSession.create(modelPath); - } - return this.session; - } - - private reshape(data: Float16Array, shape: number[]) { - const [rows, cols] = shape; - const result = []; - - for (let i = 0; i < rows; i++) { - const start = i * cols; - const end = start + cols; - - result.push(Array.from(data.subarray(start, end))); - } - - return result; - } - - private async downloadModel() { - const modelFile = Bun.file(modelPath); - if (await modelFile.exists()) { - return; - } - appLogger.info("Downloading embedding model..."); - - const info = await fileDownloadInfo({ - repo: modelName, - path: "onnx/model.onnx", - }); - - if (!info) { - appLogger.error("Cannot get download info for model file."); - return; - } - - const { url, size } = info; - let downloaded = 0; - - const response = await fetch(url); - if (!response.ok) { - appLogger.error(`Failed to download model: ${response.status} ${response.statusText}`); - return; - } - - if (!response.body) { - appLogger.error("Response body is null"); - return; - } - - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - downloaded += value.byteLength; - const percent = size > 0 ? Math.round((downloaded / size) * 100) : 0; - appLogger.info(`Embedding model download: ${percent}% (${downloaded}/${size} bytes)`); - chunks.push(value); - } - - const buffer = new Uint8Array(size); - let offset = 0; - for (const chunk of chunks) { - buffer.set(chunk, offset); - offset += chunk.byteLength; - } - - await Bun.write(modelFile, buffer); - appLogger.info("Embedding model downloaded."); - } - - public async getEmbedding(texts: string[]): Promise { - const tokenizer = await this.getTokenizer(); - const session = await this.getModelSession(); - - const { input_ids } = tokenizer(texts, { - add_special_tokens: false, - return_tensor: false, - }); - - const cumsum = (arr: number[]): number[] => { - const result: number[] = new Array(arr.length); - let currentSum = 0; - for (let i = 0; i < arr.length; i++) { - currentSum += arr[i]; - result[i] = currentSum; - } - return result; - }; - - const offsets: number[] = [ - 0, - ...cumsum(input_ids.slice(0, -1).map((x: number[]) => x.length)), - ]; - const flattened_input_ids = input_ids.flat(); - - const inputs = { - input_ids: new ort.Tensor("int64", new BigInt64Array(flattened_input_ids.map(BigInt)), [ - flattened_input_ids.length, - ]), - offsets: new ort.Tensor("int64", new BigInt64Array(offsets.map(BigInt)), [ - offsets.length, - ]), - }; - - const { embeddings } = await session.run(inputs); - return this.reshape(embeddings.data as unknown as Float16Array, [...embeddings.dims]); - } -} - -const manager = new EmbeddingManager(); -const success = await manager.init(); -export const embeddingManager = success ? manager : undefined; diff --git a/packages/embedding/src/index.ts b/packages/embedding/src/index.ts deleted file mode 100644 index b2df098..0000000 --- a/packages/embedding/src/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Elysia } from "elysia"; -import z from "zod"; -import { embeddingManager } from "./embedding"; -import type { treaty } from "@elysiajs/eden"; - -export const app = new Elysia() - .post( - "/embeddings", - async ({ body, status }) => { - const { texts } = body; - const embeddings = await embeddingManager?.getEmbedding(texts); - if (!embeddings) { - return status(500, { message: "cannot generate embedding" }); - } - return status(200, { - embeddings, - }); - }, - { - body: z.object({ - texts: z.string().array(), - }), - response: { - 200: z.object({ - embeddings: z.number().array().array(), - }), - 500: z.object({ - message: z.string(), - }), - }, - } - ) - .listen(14900); -export type EmbeddingApp = typeof app; -export type EmbeddingAppApi = ReturnType>; diff --git a/packages/embedding/tests/e2e.test.ts b/packages/embedding/tests/e2e.test.ts deleted file mode 100644 index e1ae526..0000000 --- a/packages/embedding/tests/e2e.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { treaty } from "@elysiajs/eden"; -import { app } from "../src/"; - -const api = treaty(app); - -describe("Embedding E2E Tests - POST /embeddings", () => { - test("should generate embeddings", async () => { - const payload = { - texts: ["Hello"], - }; - - const { data, status } = await api.embeddings.post(payload); - - expect(status).toBe(200); - expect(data?.embeddings[0]).toHaveLength(256); - }); -}); diff --git a/packages/embedding/tsconfig.json b/packages/embedding/tsconfig.json deleted file mode 100644 index 8417383..0000000 --- a/packages/embedding/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["./src/*.ts", "./tests/*.ts"] -} diff --git a/packages/env/package.json b/packages/env/package.json deleted file mode 100644 index 6c9d59a..0000000 --- a/packages/env/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@cvsa/env", - "type": "module", - "main": "src/index.ts", - "module": "src/index.ts", - "version": "0.1.0", - "scripts": { - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@types/bun": "latest", - "@cvsa/typescript-config": "workspace:*" - }, - "dependencies": { - "@t3-oss/env-core": "^0.13.11", - "zod": "^4.3.6" - } -} diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts deleted file mode 100644 index 1d5e9e8..0000000 --- a/packages/env/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const env = createEnv({ - server: { - DATABASE_URL: z.url().optional(), - NODE_ENV: z.enum(["development", "test", "production"]).optional(), - MEILI_MASTER_KEY: z.string().optional().default(""), - MEILI_API_URL: z.url().optional().default("http://127.0.0.1:7700"), - - OTEL_SERVICE_NAME: z.string().optional().default("cvsa-backend"), - OTEL_SERVICE_VERSION: z.string().optional().default("0.0.0"), - - LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error"]).optional().default("info"), - - REDIS_URL: z.string().optional().default("redis://127.0.0.1:6379"), - }, - - runtimeEnv: import.meta.env, - - emptyStringAsUndefined: true, -}); diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json deleted file mode 100644 index 9ed01c6..0000000 --- a/packages/env/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["src/**/*.ts", "src/*.ts"] -} diff --git a/packages/logger/package.json b/packages/logger/package.json deleted file mode 100644 index 62ee428..0000000 --- a/packages/logger/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@cvsa/logger", - "type": "module", - "main": "src/index.ts", - "module": "src/index.ts", - "version": "0.1.0", - "scripts": { - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@types/bun": "latest", - "@cvsa/typescript-config": "workspace:*" - }, - "dependencies": { - "@cvsa/env": "workspace:*", - "@cvsa/observability": "workspace:*", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.214.0", - "pino": "^10.3.1", - "pino-pretty": "^13.1.3" - } -} diff --git a/packages/logger/src/appLogger.ts b/packages/logger/src/appLogger.ts deleted file mode 100644 index b10d1f7..0000000 --- a/packages/logger/src/appLogger.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Logger as PinoLogger } from "pino"; -import { type Logger, SeverityNumber } from "@opentelemetry/api-logs"; -import { trace, SpanStatusCode, type Attributes } from "@opentelemetry/api"; -import { pinoLogger } from "./pinoSetup"; -import { otelLogger } from "@cvsa/observability"; - -type PinoLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; -type LogAttrs = Record | undefined; - -interface ApplicationLogger { - trace(msg: string, attrs?: LogAttrs): void; - debug(msg: string, attrs?: LogAttrs): void; - info(msg: string, attrs?: LogAttrs): void; - warn(msg: string, attrs?: LogAttrs): void; - error(msg: string, attrs?: LogAttrs): void; - fatal(msg: string, attrs?: LogAttrs): void; -} - -const PINO_TO_OTEL_LEVEL: Record = { - trace: SeverityNumber.TRACE, - debug: SeverityNumber.DEBUG, - info: SeverityNumber.INFO, - warn: SeverityNumber.WARN, - error: SeverityNumber.ERROR, - fatal: SeverityNumber.FATAL, -}; - -function getSpanContext(): { traceId: string | undefined; spanId: string | undefined } { - const activeSpan = trace.getActiveSpan(); - if (!activeSpan) { - return { traceId: undefined, spanId: undefined }; - } - const spanContext = activeSpan.spanContext(); - return { - traceId: spanContext.traceId || undefined, - spanId: spanContext.spanId || undefined, - }; -} - -function createLogMethod(level: PinoLevel) { - return function logMethod( - pinoLogger: PinoLogger, - otelLogger: Logger | undefined, - msg: string, - attrs?: LogAttrs - ): void { - const { traceId, spanId } = getSpanContext(); - const activeSpan = trace.getActiveSpan(); - - const spanContextAttrs = - traceId && spanId ? { trace_id: traceId, span_id: spanId } : undefined; - - const mergedAttrs = { - ...attrs, - ...spanContextAttrs, - }; - - const pinoMethod = pinoLogger[level].bind(pinoLogger) as ( - msg: string, - ...args: unknown[] - ) => void; - if (level === "error" || level === "fatal") { - const err = attrs && "error" in attrs ? (attrs.error as Error) : undefined; - if (err instanceof Error) { - pinoMethod(msg, { - ...mergedAttrs, - err, - }); - } else { - pinoMethod(msg, mergedAttrs); - } - } else { - pinoMethod(msg, mergedAttrs); - } - - if (otelLogger) { - const otelAttrs: Attributes = { - ...(attrs || {}), - ...(spanContextAttrs || {}), - }; - - if (level === "error" || level === "fatal") { - const err = attrs && "error" in attrs ? (attrs.error as Error) : undefined; - if (err) { - otelAttrs["error.object"] = String(err); - } - } - - otelLogger.emit({ - severityText: level, - severityNumber: PINO_TO_OTEL_LEVEL[level], - body: msg, - attributes: otelAttrs, - }); - } - - if (activeSpan) { - activeSpan.addEvent(msg, attrs as Attributes); - - if (level === "error" || level === "fatal") { - activeSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg }); - } - } - }; -} - -export function createApplicationLogger( - pinoLogger: PinoLogger, - otelLogger: Logger | undefined -): ApplicationLogger { - const traceMethod = createLogMethod("trace"); - const debugMethod = createLogMethod("debug"); - const infoMethod = createLogMethod("info"); - const warnMethod = createLogMethod("warn"); - const errorMethod = createLogMethod("error"); - const fatalMethod = createLogMethod("fatal"); - - return { - trace(msg: string, attrs?: LogAttrs): void { - traceMethod(pinoLogger, otelLogger, msg, attrs); - }, - debug(msg: string, attrs?: LogAttrs): void { - debugMethod(pinoLogger, otelLogger, msg, attrs); - }, - info(msg: string, attrs?: LogAttrs): void { - infoMethod(pinoLogger, otelLogger, msg, attrs); - }, - warn(msg: string, attrs?: LogAttrs): void { - warnMethod(pinoLogger, otelLogger, msg, attrs); - }, - error(msg: string, attrs?: LogAttrs): void { - errorMethod(pinoLogger, otelLogger, msg, attrs); - }, - fatal(msg: string, attrs?: LogAttrs): void { - fatalMethod(pinoLogger, otelLogger, msg, attrs); - }, - }; -} - -export const appLogger = createApplicationLogger(pinoLogger, otelLogger); diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts deleted file mode 100644 index cbb88d3..0000000 --- a/packages/logger/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { pinoLogger } from "./pinoSetup"; -export { appLogger } from "./appLogger"; - -export type { - Severity, - LogAttributes, - RequestLogAttrs, - ApplicationLogger, - RequestLogger, - OtelSdkInitResult, -} from "./types"; diff --git a/packages/logger/src/pinoSetup.ts b/packages/logger/src/pinoSetup.ts deleted file mode 100644 index 7c15274..0000000 --- a/packages/logger/src/pinoSetup.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { env } from "@cvsa/env"; -import pino, { stdTimeFunctions } from "pino"; - -function createPinoLogger(): pino.Logger { - const isProduction = env.NODE_ENV === "production"; - const isTest = env.NODE_ENV === "test"; - - const redact = ["authorization", "cookie", "password", "token"]; - - const timestamp = stdTimeFunctions.isoTime; - - const baseOptions = { - level: env.LOG_LEVEL || "info", - redact, - timestamp, - formatters: { - level: (label: string) => ({ level: label }), - }, - }; - - if (isProduction) { - return pino(baseOptions); - } - - if (isTest) { - return pino({ - ...baseOptions, - level: "warn", - transport: { - target: "pino-pretty", - options: { colorize: true }, - }, - }); - } - - return pino({ - ...baseOptions, - level: "debug", - transport: { - target: "pino-pretty", - options: { colorize: true }, - }, - }); -} - -export const pinoLogger = createPinoLogger(); -export { pino }; diff --git a/packages/logger/src/types.ts b/packages/logger/src/types.ts deleted file mode 100644 index c7b61ce..0000000 --- a/packages/logger/src/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -export type Severity = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; - -export interface LogAttributes extends Record { - userId?: string; - requestId?: string; - traceId?: string; - spanId?: string; - [key: string]: unknown; -} - -export interface RequestLogAttrs { - method: string; - path: string; - status: number; - latency: string; - query?: Record; - userAgent?: string; - ip?: string; - requestBody?: unknown; -} - -export interface ApplicationLogger { - trace(msg: string, attrs?: LogAttributes): void; - debug(msg: string, attrs?: LogAttributes): void; - info(msg: string, attrs?: LogAttributes): void; - warn(msg: string, attrs?: LogAttributes): void; - error(msg: string, attrs?: LogAttributes): void; - fatal(msg: string, attrs?: LogAttributes): void; - child(initialAttrs: LogAttributes): ApplicationLogger; -} - -export interface RequestLogger { - log(request: RequestInfo, response: ResponseInfo, duration: number): void; -} - -export interface RequestInfo { - method: string; - path: string; - query: Record; - headers: Headers; - body: unknown; -} - -export interface ResponseInfo { - status: number; - body: unknown; -} - -export interface OtelSdkInitResult { - isAvailable: boolean; - reason?: string; -} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json deleted file mode 100644 index 9ed01c6..0000000 --- a/packages/logger/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["src/**/*.ts", "src/*.ts"] -} diff --git a/packages/observability/package.json b/packages/observability/package.json deleted file mode 100644 index 5e48958..0000000 --- a/packages/observability/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@cvsa/observability", - "type": "module", - "main": "src/index.ts", - "module": "src/index.ts", - "version": "0.1.0", - "scripts": { - "typecheck": "bunx --bun tsc --noEmit" - }, - "devDependencies": { - "@types/bun": "latest", - "@cvsa/typescript-config": "workspace:*" - }, - "dependencies": { - "@cvsa/env": "workspace:*", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.214.0", - "@opentelemetry/resources": "^2.6.1", - "@opentelemetry/sdk-node": "^0.214.0" - } -} diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts deleted file mode 100644 index 39aa871..0000000 --- a/packages/observability/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./init"; -export * from "./tracer"; diff --git a/packages/observability/src/init.ts b/packages/observability/src/init.ts deleted file mode 100644 index ac462f5..0000000 --- a/packages/observability/src/init.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { logs } from "@opentelemetry/api-logs"; -import { resourceFromAttributes } from "@opentelemetry/resources"; -import { NodeSDK } from "@opentelemetry/sdk-node"; -import { env } from "@cvsa/env"; - -export async function initOtelSdk() { - const resource = resourceFromAttributes({ - "service.name": env.OTEL_SERVICE_NAME, - "service.version": env.OTEL_SERVICE_VERSION, - "deployment.environment": env.NODE_ENV ?? "development", - }); - - const sdk = new NodeSDK({ - resource, - }); - - sdk.start(); -} - -export const otelLogger = logs.getLogger(env.OTEL_SERVICE_NAME || "cvsa"); diff --git a/packages/observability/src/tracer.ts b/packages/observability/src/tracer.ts deleted file mode 100644 index d823970..0000000 --- a/packages/observability/src/tracer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SpanStatusCode, trace } from "@opentelemetry/api"; -import pkg from "../package.json"; - -export const tracer = trace.getTracer(pkg.name, pkg.version); - -export async function traceTask(name: string, fn: () => T, startTime?: number): Promise { - return tracer.startActiveSpan(name, { startTime }, async (span) => { - try { - span.setStatus({ code: SpanStatusCode.OK }); - return await fn(); - } catch (e) { - span.setStatus({ code: SpanStatusCode.ERROR }); - throw e; - } finally { - span.end(); - } - }); -} diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json deleted file mode 100644 index 9ed01c6..0000000 --- a/packages/observability/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@cvsa/typescript-config/base.json", - "include": ["src/**/*.ts", "src/*.ts"] -} diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json deleted file mode 100644 index 2805cbf..0000000 --- a/packages/typescript-config/base.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig.json", - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "types": ["bun"], - "strict": true, - "noEmit": true, - "skipLibCheck": true, - } -} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json deleted file mode 100644 index 888f841..0000000 --- a/packages/typescript-config/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@cvsa/typescript-config", - "private": true -} diff --git a/scripts/import.ts b/scripts/import.ts deleted file mode 100644 index 7085f49..0000000 --- a/scripts/import.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { extname, join, resolve } from "node:path"; -import { prisma } from "@cvsa/db"; -import { songService, artistService, singerService, artistRoleService } from "@cvsa/core"; - -interface JsonFile { - type: "song" | "album" | "series" | "singer" | "artist" | "others"; - name?: string; - songType?: string; - singers?: { name: string }[]; - artists?: { name: string; role: string }[]; - lyricsText?: string; - producer?: string[]; - bilibiliID?: string; - youtubeID?: string; - niconicoID?: string; - description?: string; - uploader?: string; - publishedAt?: string; - lyricsTTML?: string; -} - -const songTypeMap: Record = { - original: "ORIGINAL", - cover: "COVER", - retune: "RETUNE", - contrafactum: "CONTRAFACTUM", - translyrics: "TRANSLYRICS", - remix: "REMIX", - remaster: "REMASTER", - mashup: "MASHUP", - instrumental: "INSTRUMENTAL", - others: "OTHERS", -}; - -async function resolveSinger(name: string) { - const existing = await prisma.singer.findFirst({ where: { name, deletedAt: null } }); - if (existing) return existing.id; - const created = await singerService.create({ name }); - return created.id; -} - -async function resolveArtist(name: string) { - const existing = await prisma.artist.findFirst({ where: { name, deletedAt: null } }); - if (existing) return existing.id; - const created = await artistService.create({ name }); - return created.id; -} - -async function resolveRole(name: string) { - const existing = await prisma.artistRole.findFirst({ where: { name, deletedAt: null } }); - if (existing) return existing.id; - const created = await artistRoleService.create({ name }); - return created.id; -} - -async function importSinger(data: JsonFile) { - const result = await singerService.create({ name: data.name }); - console.log(`singer → ${result.id}`); -} - -async function importArtist(data: JsonFile) { - const result = await artistService.create({ name: data.name }); - console.log(`artist → ${result.id}`); -} - -async function importSong(data: JsonFile) { - const performances: { singerId: number }[] = []; - if (data.singers) { - for (const s of data.singers) { - performances.push({ singerId: await resolveSinger(s.name) }); - } - } - - const creations: { artistId: number; roleId: number }[] = []; - if (data.artists) { - for (const a of data.artists) { - creations.push({ - artistId: await resolveArtist(a.name), - roleId: await resolveRole(a.role), - }); - } - } - if (data.producer) { - const roleId = await resolveRole("Producer"); - for (const name of data.producer) { - creations.push({ artistId: await resolveArtist(name), roleId }); - } - } - if (data.uploader) { - const roleId = await resolveRole("Uploader"); - creations.push({ artistId: await resolveArtist(data.uploader), roleId }); - } - - const lyrics: { plainText?: string; ttml?: string; language: string }[] = []; - if (data.lyricsText) lyrics.push({ plainText: data.lyricsText, language: "zh" }); - if (data.lyricsTTML) { - const existing = lyrics.find((l) => l.language === "zh" && l.plainText); - if (existing) existing.ttml = data.lyricsTTML; - else lyrics.push({ ttml: data.lyricsTTML, language: "zh" }); - } - - const song = await songService.create({ - name: data.name, - type: data.songType ? (songTypeMap[data.songType] as never) : undefined, - description: data.description, - publishedAt: new Date(data.publishedAt ?? "").toISOString(), - performances: performances.length ? performances : undefined, - creations: creations.length ? creations : undefined, - lyrics: lyrics.length ? lyrics : undefined, - }); - - if (data.bilibiliID) { - const updateData: Record = {}; - if (/^BV/i.test(data.bilibiliID)) updateData.bilibiliBvid = data.bilibiliID; - else if (/^\d+$/.test(data.bilibiliID)) updateData.bilibiliAid = BigInt(data.bilibiliID); - if (Object.keys(updateData).length) { - await prisma.song.update({ where: { id: song.id }, data: updateData }); - } - } - - if (data.youtubeID) { - await prisma.externalLink.create({ - data: { - songId: song.id, - url: `https://youtube.com/watch?v=${data.youtubeID}`, - platform: "YOUTUBE", - platformId: data.youtubeID, - }, - }); - } - if (data.niconicoID) { - await prisma.externalLink.create({ - data: { - songId: song.id, - url: `https://nicovideo.jp/watch/${data.niconicoID}`, - platform: "NICONICO", - platformId: data.niconicoID, - }, - }); - } - - console.log(`song → ${song.id}`); -} - -async function main() { - const folder = process.argv[2]; - if (!folder) { - console.error("Usage: bun run scripts/import.ts "); - process.exit(1); - } - - const folderPath = resolve(folder); - if (!existsSync(folderPath)) { - console.error("not found"); - process.exit(1); - } - - const files = readdirSync(folderPath) - .filter((f) => extname(f) === ".json") - .sort(); - - for (const file of files) { - try { - const data: JsonFile = JSON.parse(readFileSync(join(folderPath, file), "utf-8")); - switch (data.type) { - case "singer": - await importSinger(data); - break; - case "artist": - await importArtist(data); - break; - case "song": - await importSong(data); - break; - default: - await importSinger(data); - console.warn(`unkown type ${data.type} for file ${file}`); - } - } catch (e) { - console.error("error at: ", file); - console.error(e); - } - } -} - -main() - .catch((err) => { - console.error("fatal:", err); - process.exit(1); - }) - .finally(() => { - console.log("completed"); - process.exit(0); - }); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index 0d3e8db..0000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../packages/typescript-config/base.json", - "compilerOptions": { - "noEmit": true, - "paths": { - "@cvsa/db": ["../packages/db/src"], - "@cvsa/core": ["../packages/core/src"], - "@cvsa/core/internal": ["../packages/core/src/internal"], - "@cvsa/env": ["../packages/env/src"], - "@cvsa/logger": ["../packages/logger/src"], - "@cvsa/observability": ["../packages/observability/src"], - "@cvsa/embedding": ["../packages/embedding/src"] - } - }, - "include": ["./**/*.ts"] -} diff --git a/turbo.json b/turbo.json index cd1c75c..7303a6a 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,4 @@ { -<<<<<<< HEAD "$schema": "https://turbo.build/schema.json", "tasks": { "build": { @@ -16,66 +15,4 @@ "outputs": ["out/**"] } } -======= - "remoteCache": { - "enabled": true - }, - "$schema": "https://turbo.build/schema.json", - "ui": "tui", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**", ".output/**"] - }, - "dev": { - "cache": false, - "persistent": true - }, - "lint": { - "inputs": ["tests/**.ts", "src/**.ts", "biome.json", ".biome*"] - }, - "test": { - "inputs": [ - "src/**.ts", - "tests/**.ts", - "tsconfig.json", - "$TURBO_ROOT$/packages/typescript-config/base.json" - ] - }, - "test:coverage": {}, - "setup": { - "cache": false, - "dependsOn": ["@cvsa/db#push", "@cvsa/db#test:setup", "@cvsa/db#gen"] - }, - "setup:ci": { - "cache": false, - "dependsOn": ["@cvsa/db#test:setup", "@cvsa/db#gen"] - }, - "@cvsa/db#test:setup": { - "cache": false - }, - "@cvsa/db#push": { - "cache": false - }, - "@cvsa/db#gen": { - "cache": false - }, - "typecheck": { - "inputs": [ - "src/**.ts", - "tsconfig.json", - "$TURBO_ROOT$/packages/typescript-config/base.json" - ] - }, - "@cvsa/db#test:reset": { - "cache": false - }, - "@cvsa/core#test:reset": { - "cache": false - }, - "//test:reset": { - "dependsOn": ["@cvsa/db#test:reset", "@cvsa/core#test:reset"] - } - } ->>>>>>> origin/develop } From f685c4f6b17bb827d04066156805b6773eb4ba98 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 04:09:22 +0800 Subject: [PATCH 35/37] refactor(docs): comply type rules for SEO/GEO files - Replace `as Lang` inline assertions with `isLang` type guard in i18n.ts - Replace `tree: any` with `Root` from `hast` in source.config.ts - Replace `.then()` chaining with `async/await` in PageActions - Rename component files to PascalCase: Links, PageActions, UpdateLanguage - Add `@types/hast` as dev dependency for proper hast type definitions --- apps/docs/components/{links.tsx => Links.tsx} | 0 apps/docs/components/{page-actions.tsx => PageActions.tsx} | 0 apps/docs/components/{update-language.tsx => UpdateLanguage.tsx} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename apps/docs/components/{links.tsx => Links.tsx} (100%) rename apps/docs/components/{page-actions.tsx => PageActions.tsx} (100%) rename apps/docs/components/{update-language.tsx => UpdateLanguage.tsx} (100%) diff --git a/apps/docs/components/links.tsx b/apps/docs/components/Links.tsx similarity index 100% rename from apps/docs/components/links.tsx rename to apps/docs/components/Links.tsx diff --git a/apps/docs/components/page-actions.tsx b/apps/docs/components/PageActions.tsx similarity index 100% rename from apps/docs/components/page-actions.tsx rename to apps/docs/components/PageActions.tsx diff --git a/apps/docs/components/update-language.tsx b/apps/docs/components/UpdateLanguage.tsx similarity index 100% rename from apps/docs/components/update-language.tsx rename to apps/docs/components/UpdateLanguage.tsx From 195e06d1abd74b0748b309c9d59dfe7397045055 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 04:12:30 +0800 Subject: [PATCH 36/37] =?UTF-8?q?chore:=20add=20missing=20files=20from=20p?= =?UTF-8?q?revious=20commit=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx | 2 +- apps/docs/app/(landing)/layout.tsx | 2 +- apps/docs/app/(landing)/old/page.tsx | 2 +- apps/docs/app/(landing)/page.tsx | 2 +- apps/docs/app/not-found.tsx | 2 +- apps/docs/components/PageActions.tsx | 5 ++++- apps/docs/lib/i18n.ts | 8 ++++++-- apps/docs/package.json | 1 + apps/docs/source.config.ts | 4 ++-- bun.lock | 1 + 10 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx index 7a02d08..5fb5d85 100644 --- a/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/(docs)/[lang]/[[...slug]]/page.tsx @@ -5,7 +5,7 @@ import { getMDXComponents } from "@/components/mdx"; import type { Metadata } from "next"; import { createRelativeLink } from "fumadocs-ui/mdx"; import { gitConfig } from "@/lib/layout.shared"; -import { MarkdownCopyButton, ViewOptionsPopover } from "@/components/page-actions"; +import { MarkdownCopyButton, ViewOptionsPopover } from "@/components/PageActions"; import { buttonVariants } from "fumadocs-ui/components/ui/button"; import { t } from "@/lib/i18n"; import { siteUrl } from "@/lib/env"; diff --git a/apps/docs/app/(landing)/layout.tsx b/apps/docs/app/(landing)/layout.tsx index cda2276..1d04c88 100644 --- a/apps/docs/app/(landing)/layout.tsx +++ b/apps/docs/app/(landing)/layout.tsx @@ -1,6 +1,6 @@ import "./global.css"; import { Funnel_Sans } from "next/font/google"; -import { UpdateLanguage } from "@/components/update-language"; +import { UpdateLanguage } from "@/components/UpdateLanguage"; import type { Metadata } from "next"; import { siteUrl } from "@/lib/env"; import { t } from "@/lib/i18n"; diff --git a/apps/docs/app/(landing)/old/page.tsx b/apps/docs/app/(landing)/old/page.tsx index d637928..178581d 100644 --- a/apps/docs/app/(landing)/old/page.tsx +++ b/apps/docs/app/(landing)/old/page.tsx @@ -1,4 +1,4 @@ -import { HeroLinks } from "@/components/links"; +import { HeroLinks } from "@/components/Links"; import { LogoEnglish } from "@/components/icon/LogoEnglish"; export default function Page() { diff --git a/apps/docs/app/(landing)/page.tsx b/apps/docs/app/(landing)/page.tsx index 44c9b33..3de34ed 100644 --- a/apps/docs/app/(landing)/page.tsx +++ b/apps/docs/app/(landing)/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { HeroLinks } from "@/components/links"; +import { HeroLinks } from "@/components/Links"; import { LogoEnglish } from "@/components/icon/LogoEnglish"; import { DigitGrid } from "./DigitGrid"; import { Description } from "./Description"; diff --git a/apps/docs/app/not-found.tsx b/apps/docs/app/not-found.tsx index cfdecc2..fb00c03 100644 --- a/apps/docs/app/not-found.tsx +++ b/apps/docs/app/not-found.tsx @@ -1,6 +1,6 @@ import "./global.css"; import { Inter } from "next/font/google"; -import { UpdateLanguage } from "@/components/update-language"; +import { UpdateLanguage } from "@/components/UpdateLanguage"; import type { Metadata } from "next"; import { SITE_TITLE_404, SITE_DESCRIPTION_404, ogImageUrl, OG_IMAGE_SIZE } from "@/lib/metadata"; diff --git a/apps/docs/components/PageActions.tsx b/apps/docs/components/PageActions.tsx index d4de0ac..8cf94df 100644 --- a/apps/docs/components/PageActions.tsx +++ b/apps/docs/components/PageActions.tsx @@ -27,7 +27,10 @@ export function MarkdownCopyButton({ setLoading(true); try { - const promise = fetch(markdownUrl).then((res) => res.text()); + const promise = (async () => { + const res = await fetch(markdownUrl); + return res.text(); + })(); cache.set(markdownUrl, promise); await navigator.clipboard.write([ new ClipboardItem({ diff --git a/apps/docs/lib/i18n.ts b/apps/docs/lib/i18n.ts index 5b3350c..93ddc1b 100644 --- a/apps/docs/lib/i18n.ts +++ b/apps/docs/lib/i18n.ts @@ -38,9 +38,13 @@ type Resource = typeof resources; type Lang = keyof Resource; type MessageKey = keyof Resource[Lang]; +function isLang(lang: string): lang is Lang { + return lang in resources; +} + export function t(key: MessageKey, lang: string): string { - if (resources[lang as Lang] === undefined) { + if (!isLang(lang)) { return key; } - return resources[lang as Lang][key] || key; + return resources[lang][key] || key; } diff --git a/apps/docs/package.json b/apps/docs/package.json index 2420d5e..c438fbd 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.2.1", + "@types/hast": "^3.0.4", "@types/mdx": "^2.0.13", "@types/node": "^25.5.0", "@types/react": "^19.2.14", diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 1183444..59dfd10 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -1,6 +1,7 @@ import { defineConfig, defineDocs } from "fumadocs-mdx/config"; import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; import { visit } from "unist-util-visit"; +import type { Root } from "hast"; import { remarkMdxMermaid } from 'fumadocs-core/mdx-plugins'; // You can customise Zod schemas for frontmatter and `meta.json` here @@ -19,8 +20,7 @@ export const docs = defineDocs({ }); function rehypeFootnote() { - // biome-ignore lint/suspicious/noExplicitAny: I don't really know the type. - return (tree: any) => { + return (tree: Root) => { visit(tree, "element", (node) => { if (node.tagName === "h2" && node.properties?.id === "footnote-label") { node.children = []; diff --git a/bun.lock b/bun.lock index 822d287..2bcc7d5 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.2.1", + "@types/hast": "^3.0.4", "@types/mdx": "^2.0.13", "@types/node": "^25.5.0", "@types/react": "^19.2.14", From ac235b90e38fb3e7f756026140974d2639e236c8 Mon Sep 17 00:00:00 2001 From: 13812773784 Date: Tue, 16 Jun 2026 04:43:46 +0800 Subject: [PATCH 37/37] chore(ci/docs): CI config details and line endings tweaks --- .github/workflows/build-docs.yml | 4 ++-- apps/docs/app/sitemap.ts | 2 +- apps/docs/lib/env.ts | 2 +- apps/docs/lib/metadata.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 596a00e..059622e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -11,7 +11,6 @@ concurrency: group: pages-deploy cancel-in-progress: true -jobs: jobs: deploy: runs-on: ubuntu-latest @@ -47,4 +46,5 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy apps/docs/out --project-name="cvsa-docs" \ No newline at end of file + command: pages deploy apps/docs/out --project-name="cvsa-docs" + \ No newline at end of file diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts index f9c2ee9..99ed5f0 100644 --- a/apps/docs/app/sitemap.ts +++ b/apps/docs/app/sitemap.ts @@ -25,4 +25,4 @@ export default function sitemap(): MetadataRoute.Sitemap { }; return [landingPage, ...docPages]; -} \ No newline at end of file +} diff --git a/apps/docs/lib/env.ts b/apps/docs/lib/env.ts index 956f517..1362fc2 100644 --- a/apps/docs/lib/env.ts +++ b/apps/docs/lib/env.ts @@ -4,4 +4,4 @@ * Falls back to production URL during build. */ const raw = process.env.NEXT_PUBLIC_SITE_URL || "https://docs.projectcvsa.com"; -export const siteUrl = raw.replace(/\/$/, ""); \ No newline at end of file +export const siteUrl = raw.replace(/\/$/, ""); diff --git a/apps/docs/lib/metadata.ts b/apps/docs/lib/metadata.ts index 9ef61a9..accd902 100644 --- a/apps/docs/lib/metadata.ts +++ b/apps/docs/lib/metadata.ts @@ -20,4 +20,4 @@ export const OG_IMAGE_SIZE = 512; */ export function ogImageUrl(): string { return `${siteUrl}/android-chrome-512x512.png`; -} \ No newline at end of file +}