├── .github └── workflows │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker_start.sh ├── knexfile.js ├── migrations ├── 20220222172635_initial.js ├── 20220307101400_timetolive.js ├── 20220316202408_coach_sequence_origin_destination.js ├── 20220801124814_building_series.js ├── 20220830162909_train_type_ic.js ├── 20220901170052_cascade.js ├── 20221101203512_building_series_name.js ├── 20221108160833_cascade_2.js ├── 20221113184617_ic_1.js ├── 20221113214622_delay.js ├── 20221114074228_create_delays.js └── 20230622172819_cascade_3.js ├── package.json ├── src ├── autoFetch.ts ├── cache.ts ├── database.ts ├── dateTimeFormat.ts ├── fetcher │ ├── bahn_expert.ts │ └── request.ts ├── heartbeat.ts ├── index.ts ├── logger.ts ├── metrics.ts ├── rabbit │ ├── fetch_coach_sequence.ts │ ├── fetch_train_details.ts │ ├── fetch_train_numbers.ts │ └── rabbit.ts ├── rabbitAsyncHandler.ts ├── redis.ts └── staticConfig.ts ├── tsconfig.json └── yarn.lock /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | branches: [ main, canary ] 11 | 12 | env: 13 | # Use docker.io for Docker Hub if empty 14 | REGISTRY: ghcr.io 15 | # github.repository as / 16 | IMAGE_NAME: ${{ github.repository }} 17 | 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | # This is used to complete the identity challenge 27 | # with sigstore/fulcio when running outside of PRs. 28 | id-token: write 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v2 33 | 34 | # Workaround: https://github.com/docker/build-push-action/issues/461 35 | - name: Setup Docker buildx 36 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 37 | 38 | # Login against a Docker registry except on PR 39 | # https://github.com/docker/login-action 40 | - name: Log into registry ${{ env.REGISTRY }} 41 | if: github.event_name != 'pull_request' 42 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 43 | with: 44 | registry: ${{ env.REGISTRY }} 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | # Extract metadata (tags, labels) for Docker 49 | # https://github.com/docker/metadata-action 50 | - name: Extract Docker metadata 51 | id: meta 52 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 53 | with: 54 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 55 | 56 | # Build and push Docker image with Buildx (don't push on PR) 57 | # https://github.com/docker/build-push-action 58 | - name: Build and push Docker image 59 | id: build-and-push 60 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 61 | with: 62 | context: . 63 | push: ${{ github.event_name != 'pull_request' }} 64 | tags: ${{ steps.meta.outputs.tags }} 65 | labels: ${{ steps.meta.outputs.labels }} 66 | 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as base 2 | WORKDIR /app 3 | 4 | COPY package.json yarn.lock ./ 5 | 6 | FROM base as dependencies 7 | 8 | RUN yarn install --cache-folder ./ycache --immutable --immutable-cache --production=false 9 | RUN rm -rf ./yache 10 | 11 | FROM dependencies as build 12 | 13 | ENV NODE_ENV=production 14 | COPY src ./src 15 | COPY knexfile.js tsconfig.json ./ 16 | RUN yarn run build 17 | 18 | 19 | FROM node:16-alpine 20 | WORKDIR /app 21 | 22 | ENV NODE_ENV=production 23 | ENV REDIS_URL= 24 | ENV RABBIT_URL= 25 | 26 | ENV MYSQL_HOST= 27 | ENV MYSQL_PORT=3306 28 | ENV MYSQL_DATABASE=regenbogenice 29 | ENV MYSQL_USER=regenbogenice 30 | ENV MYSQL_PASSWORD= 31 | 32 | ENV METRIC_HTTP_HOST=0.0.0.0 33 | ENV METRIC_HTTP_PORT=9000 34 | 35 | COPY --from=build /app/node_modules/ ./node_modules/ 36 | COPY --from=build /app/dist/ ./dist/ 37 | COPY --from=build /app/knexfile.js . 38 | COPY --from=build /app/package.json ./ 39 | COPY ./docker_start.sh . 40 | COPY ./migrations/ ./migrations 41 | RUN chmod a+x docker_start.sh 42 | 43 | EXPOSE 80 44 | CMD ["/app/docker_start.sh"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backend -------------------------------------------------------------------------------- /docker_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | yarn run knex --env production migrate:latest 4 | yarn node dist/src/index.js -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | 3 | config() 4 | 5 | 6 | /** 7 | * @type { Object. } 8 | */ 9 | export default { 10 | 11 | development: { 12 | client: 'mysql2', 13 | connection: { 14 | host: process.env.MYSQL_HOST || '127.0.0.1', 15 | port: process.env.MYSQL_PORT || '3306', 16 | database: process.env.MYSQL_DATABASE || 'regenbogenice', 17 | user: process.env.MYSQL_USER || 'root', 18 | password: process.env.MYSQL_PASSWORD || 'root', 19 | timezone: '+00:00', 20 | }, 21 | }, 22 | 23 | production: { 24 | client: 'mysql2', 25 | connection: { 26 | host: process.env.MYSQL_HOST || '127.0.0.1', 27 | port: process.env.MYSQL_PORT || '3306', 28 | database: process.env.MYSQL_DATABASE || 'regenbogenice', 29 | user: process.env.MYSQL_USER || 'regenbogenice', 30 | password: process.env.MYSQL_PASSWORD, 31 | timezone: '+00:00', 32 | }, 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /migrations/20220222172635_initial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | 6 | // https://dbdiagram.io/d/61c8e8743205b45b73cd18c5 7 | 8 | export const up = async (knex) => { 9 | await knex.schema.createTable('train_vehicle', (table) => { 10 | table.increments('id') 11 | table.integer('train_vehicle_number').notNullable().unique() 12 | table.string('train_vehicle_name', 255) 13 | table.string('train_type', 3).notNullable() 14 | table.integer('building_series').notNullable() 15 | table.timestamp('timestamp').defaultTo(knex.fn.now()) 16 | }) 17 | 18 | await knex.schema.createTable('train_trip', (table) => { 19 | table.increments('id') 20 | table.string('train_type', 3).notNullable() 21 | table.integer('train_number').notNullable() 22 | table.integer('origin_station') 23 | table.integer('destination_station') 24 | table.timestamp('initial_departure').notNullable() 25 | table.timestamp('timestamp').defaultTo(knex.fn.now()) 26 | }) 27 | 28 | await knex.schema.createTable('train_trip_route', (table) => { 29 | table.increments('id') 30 | table.integer('train_trip_id').unsigned().notNullable() 31 | table.foreign('train_trip_id').references('id').inTable('train_trip') 32 | table.integer('index').notNullable() 33 | table.boolean('cancelled').notNullable().defaultTo(false) 34 | table.integer('station').notNullable() 35 | table.timestamp('scheduled_departure') 36 | table.timestamp('departure') 37 | table.timestamp('scheduled_arrival') 38 | table.timestamp('arrival') 39 | }) 40 | 41 | await knex.schema.createTable('train_trip_vehicle', (table) => { 42 | table.increments('id') 43 | table.integer('train_vehicle_id').unsigned().notNullable() 44 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle') 45 | table.integer('train_trip_id').unsigned().notNullable() 46 | table.foreign('train_trip_id').references('id').inTable('train_trip') 47 | table.integer('group_index').notNullable() 48 | table.timestamp('timestamp').defaultTo(knex.fn.now()) 49 | }) 50 | 51 | await knex.schema.createTable('train_trip_vehicle_change', (table) => { 52 | table.increments('id') 53 | table.integer('train_vehicle_id').unsigned().notNullable() 54 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle') 55 | table.integer('train_trip_id').unsigned().notNullable() 56 | table.foreign('train_trip_id').references('id').inTable('train_trip') 57 | table.integer('group_index').notNullable() 58 | table.timestamp('timestamp').defaultTo(knex.fn.now()) 59 | table.timestamp('original_timestamp').notNullable() 60 | }) 61 | 62 | await knex.schema.createTable('coach_sequence', (table) => { 63 | table.increments('id') 64 | table.integer('train_vehicle_id').unsigned().notNullable() 65 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle') 66 | table.timestamp('timestamp').defaultTo(knex.fn.now()) 67 | }) 68 | 69 | await knex.schema.createTable('coach', (table) => { 70 | table.increments('id') 71 | table.integer('index').notNullable() 72 | table.integer('coach_sequence_id').unsigned().notNullable() 73 | table.foreign('coach_sequence_id').references('id').inTable('coach_sequence') 74 | table.string('uic', 12).notNullable() 75 | table.string('category') 76 | table.integer('class') 77 | table.string('type') 78 | }) 79 | 80 | await knex.schema.createTable('train_trip_coaches_identification', (table) => { 81 | table.increments('id') 82 | table.integer('train_trip_id').unsigned().notNullable() 83 | table.foreign('train_trip_id').references('id').inTable('train_trip') 84 | table.integer('coach_id').unsigned().notNullable() 85 | table.foreign('coach_id').references('id').inTable('coach') 86 | table.integer('identification_number') 87 | }) 88 | } 89 | 90 | /** 91 | * @param { import("knex").Knex } knex 92 | * @returns { Promise } 93 | */ 94 | 95 | export const down = async (knex) => { 96 | await knex.schema.dropTable('train_vehicle') 97 | await knex.schema.dropTable('train_trip') 98 | await knex.schema.dropTable('train_trip_vehicle') 99 | await knex.schema.dropTable('train_trip_vehicle_change') 100 | await knex.schema.dropTable('coach_sequence') 101 | await knex.schema.dropTable('coach') 102 | await knex.schema.dropTable('train_trip_coaches_identification') 103 | } 104 | -------------------------------------------------------------------------------- /migrations/20220307101400_timetolive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.alterTable('train_trip', table => { 7 | table.timestamp('routes_update_expire') 8 | table.timestamp('coach_sequence_update_expire') 9 | }) 10 | } 11 | 12 | /** 13 | * @param { import("knex").Knex } knex 14 | * @returns { Promise } 15 | */ 16 | export const down = async (knex) => { 17 | await knex.schema.alterTable('train_trip', table => { 18 | table.dropColumn('routes_update_expire') 19 | table.dropColumn('coach_sequence_update_expire') 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /migrations/20220316202408_coach_sequence_origin_destination.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.alterTable('train_trip_vehicle', table => { 7 | table.integer('origin') 8 | table.integer('destination') 9 | }) 10 | await knex.schema.alterTable('train_trip_vehicle_change', table => { 11 | table.integer('origin') 12 | table.integer('destination') 13 | }) 14 | } 15 | 16 | /** 17 | * @param { import("knex").Knex } knex 18 | * @returns { Promise } 19 | */ 20 | export const down = async (knex) => { 21 | await knex.schema.alterTable('train_trip_vehicle', table => { 22 | table.dropColumn('origin') 23 | table.dropColumn('destination') 24 | }) 25 | await knex.schema.alterTable('train_trip_vehicle_change', table => { 26 | table.dropColumn('origin') 27 | table.dropColumn('destination') 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /migrations/20220801124814_building_series.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.alterTable('train_vehicle', table => { 7 | table.setNullable('building_series') 8 | }) 9 | } 10 | 11 | /** 12 | * @param { import("knex").Knex } knex 13 | * @returns { Promise } 14 | */ 15 | export const down = async (knex) => { 16 | await knex.schema.alterTable('train_trip_vehicle', table => { 17 | table.dropNullable('building_series') 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /migrations/20220830162909_train_type_ic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex('train_vehicle').where({ train_type: 'IC' }).update({ train_type: 'ICK' }) 7 | } 8 | 9 | /** 10 | * @param { import("knex").Knex } knex 11 | * @returns { Promise } 12 | */ 13 | export const down = async (knex) => { 14 | await knex('train_vehicle').where({ train_type: 'ICK' }).orWhere({ train_type: 'ICD' }).update({ train_type: 'IC' }) 15 | } 16 | -------------------------------------------------------------------------------- /migrations/20220901170052_cascade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.table('train_trip_vehicle', table => { 7 | table.dropForeign('train_trip_id') 8 | table.foreign('train_trip_id').references('id').inTable('train_trip').onDelete('CASCADE') 9 | table.dropForeign('train_vehicle_id') 10 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle').onDelete('CASCADE') 11 | }) 12 | 13 | await knex.schema.table('train_trip_route', table => { 14 | table.dropForeign('train_trip_id') 15 | table.foreign('train_trip_id').references('id').inTable('train_trip').onDelete('CASCADE') 16 | }) 17 | } 18 | 19 | /** 20 | * @param { import("knex").Knex } knex 21 | * @returns { Promise } 22 | */ 23 | export const down = async (knex) => { 24 | await knex.schema.table('train_trip_vehicle', table => { 25 | table.dropForeign('train_trip_id') 26 | table.foreign('train_trip_id').references('id').inTable('train_trip') 27 | table.dropForeign('train_vehicle_id') 28 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle') 29 | }) 30 | 31 | await knex.schema.table('train_trip_route', table => { 32 | table.dropForeign('train_trip_id') 33 | table.foreign('train_trip_id').references('id').inTable('train_trip').onDelete('CASCADE') 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /migrations/20221101203512_building_series_name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.table('train_vehicle', table => { 7 | table.string('building_series_name', 255) 8 | }) 9 | } 10 | 11 | /** 12 | * @param { import("knex").Knex } knex 13 | * @returns { Promise } 14 | */ 15 | export const down = async (knex) => { 16 | await knex.schema.table('train_vehicle', table => { 17 | table.dropColumn('building_series_name') 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /migrations/20221108160833_cascade_2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.table('train_trip_vehicle_change', table => { 7 | table.dropForeign('train_trip_id') 8 | table.foreign('train_trip_id').references('id').inTable('train_trip').onDelete('CASCADE') 9 | table.dropForeign('train_vehicle_id') 10 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle').onDelete('CASCADE') 11 | }) 12 | } 13 | 14 | /** 15 | * @param { import("knex").Knex } knex 16 | * @returns { Promise } 17 | */ 18 | export const down = async (knex) => { 19 | await knex.schema.table('train_trip_vehicle_change', table => { 20 | table.dropForeign('train_trip_id') 21 | table.foreign('train_trip_id').references('id').inTable('train_trip') 22 | table.dropForeign('train_vehicle_id') 23 | table.foreign('train_vehicle_id').references('id').inTable('train_vehicle') 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /migrations/20221113184617_ic_1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.alterTable('coach', table => { 7 | table.dropForeign('coach_sequence_id') 8 | table.integer('coach_sequence_id').unsigned().alter() 9 | table.integer('index').alter() 10 | table.foreign('coach_sequence_id').references('id').inTable('coach_sequence') 11 | }) 12 | } 13 | 14 | /** 15 | * @param { import("knex").Knex } knex 16 | * @returns { Promise } 17 | */ 18 | export const down = async (knex) => { 19 | await knex.schema.alterTable('coach', table => { 20 | table.dropForeign('coach_sequence_id') 21 | table.integer('coach_sequence_id').unsigned().notNullable().alter() 22 | table.integer('index').notNullable().alter() 23 | table.foreign('coach_sequence_id').references('id').inTable('coach_sequence') 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /migrations/20221113214622_delay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.alterTable('train_trip_route', table => { 7 | table.integer('arrival_delay') 8 | table.integer('departure_delay') 9 | }) 10 | } 11 | 12 | /** 13 | * @param { import("knex").Knex } knex 14 | * @returns { Promise } 15 | */ 16 | export const down = async (knex) => { 17 | await knex.schema.alterTable('train_trip_route', table => { 18 | table.dropColumn('arrival_delay') 19 | table.dropColumn('departure_delay') 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /migrations/20221114074228_create_delays.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { toSQLTimestamp } from '../dist/src/dateTimeFormat.js' 3 | 4 | /** 5 | * @param { import("knex").Knex } knex 6 | * @returns { Promise } 7 | */ 8 | export const up = async (knex) => { 9 | const since = DateTime.now().minus({ months: 3 }) 10 | const routes = await knex('train_trip_route') 11 | .where(builder => { 12 | builder.where('scheduled_departure', '>', toSQLTimestamp(since)) 13 | .orWhere('scheduled_arrival', '>', toSQLTimestamp(since)) 14 | }) 15 | .andWhere(builder => { 16 | builder.whereNull('arrival_delay') 17 | .whereNull('departure_delay') 18 | }) 19 | .select(['id', 'scheduled_arrival', 'arrival', 'scheduled_departure', 'departure']) 20 | for (const route of routes) { 21 | let u = {} 22 | if (route.scheduled_arrival && route.arrival) { 23 | u['arrival_delay'] = DateTime.fromJSDate(route.arrival).diff(DateTime.fromJSDate(route.scheduled_arrival)).as('minutes') 24 | } 25 | if (route.scheduled_departure && route.departure) { 26 | u['departure_delay'] = DateTime.fromJSDate(route.departure).diff(DateTime.fromJSDate(route.scheduled_departure)).as('minutes') 27 | } 28 | await knex('train_trip_route').where({ id: route.id }).update(u) 29 | } 30 | console.log(`Run create_delays migration for ${routes.length} routes.`) 31 | } 32 | 33 | /** 34 | * @param { import("knex").Knex } knex 35 | * @returns { Promise } 36 | */ 37 | export const down = async (knex) => { 38 | 39 | } 40 | -------------------------------------------------------------------------------- /migrations/20230622172819_cascade_3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | export const up = async (knex) => { 6 | await knex.schema.table('train_trip_coaches_identification', table => { 7 | table.dropForeign('train_trip_id') 8 | table.foreign('train_trip_id').references('id').inTable('train_trip').onDelete('CASCADE') 9 | table.dropForeign('coach_id') 10 | table.foreign('coach_id').references('id').inTable('coach').onDelete('CASCADE') 11 | }) 12 | } 13 | 14 | /** 15 | * @param { import("knex").Knex } knex 16 | * @returns { Promise } 17 | */ 18 | export const down = async (knex) => { 19 | await knex.schema.table('train_trip_coaches_identification', table => { 20 | table.dropForeign('train_trip_id') 21 | table.foreign('train_trip_id').references('id').inTable('train_trip') 22 | table.dropForeign('coach_id') 23 | table.foreign('coach_id').references('id').inTable('coach') 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regenbogen-ice-backend", 3 | "version": "1.0.0", 4 | "main": "dist/src/index.js", 5 | "repository": "https://github.com/regenbogen-ice/backend.git", 6 | "author": "AdriDevelopsThings, PhilippIRL", 7 | "license": "AGPL-3.0", 8 | "private": true, 9 | "type": "module", 10 | "devDependencies": { 11 | "@types/express": "^4.17.15", 12 | "@types/luxon": "^2.0.9", 13 | "@types/node": "^17.0.19", 14 | "@types/node-fetch": "^2.6.1", 15 | "typescript": "^4.5.5" 16 | }, 17 | "dependencies": { 18 | "@sentry/node": "^7.9.0", 19 | "dotenv": "^16.0.0", 20 | "express": "^4.18.2", 21 | "knex": "^2.4.0", 22 | "luxon": "^2.3.1", 23 | "mysql2": "^2.3.3", 24 | "node-fetch": "^3.2.10", 25 | "rabbit-queue": "^5.4.1", 26 | "redis": "^4.0.4", 27 | "shoutrrrd-js": "^1.0.3" 28 | }, 29 | "scripts": { 30 | "build": "tsc", 31 | "start": "tsc && node dist/src/index.js" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/autoFetch.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import { info, debug } from './logger.js' 3 | import { rabbit } from './rabbit/rabbit.js' 4 | import staticConfig from './staticConfig.js' 5 | import heartbeat from './heartbeat.js' 6 | 7 | let fetchedHour: number | null = null 8 | 9 | export const autoFetch = () => { 10 | heartbeat() 11 | const hour = DateTime.now().hour 12 | if (fetchedHour != hour) { 13 | fetchedHour = hour 14 | if (process.env.DISABLE_AUTOFETCH == 'true') { 15 | debug(`Autofetch disabled.`) 16 | } else { 17 | info(`Autofetch running.`) 18 | rabbit.publish('fetch_train_numbers', { evaNumbers: staticConfig.AUTO_FETCH_EVA_NUMBERS }) 19 | } 20 | } 21 | setTimeout(autoFetch, 1000 * 60) 22 | } -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import redis from './redis.js' 2 | 3 | export class Cache { 4 | constructor (private namespace: string, private ttl: number) { } 5 | 6 | async get (key: string): Promise { 7 | const redisResponse = await redis.get(`${this.namespace}_${key}`) 8 | if (redisResponse) { 9 | return JSON.parse(redisResponse) 10 | } 11 | 12 | } 13 | 14 | async set (key: string, value: any, ttl?: number) { 15 | if (!ttl) { 16 | ttl = this.ttl 17 | } 18 | await redis.set(`${this.namespace}_${key}`, JSON.stringify(value), { EX: ttl }) 19 | } 20 | 21 | async remove (key: string) { 22 | await redis.del(`${this.namespace}_${key}`) 23 | } 24 | } -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import knex from 'knex' 2 | import knexfile from '../knexfile.js' 3 | 4 | export default knex(knexfile[process.env.NODE_ENV || 'development']) -------------------------------------------------------------------------------- /src/dateTimeFormat.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | 3 | export const fromBahnExpertTimestamp = (bahnExpertTimestamp: string) => DateTime.fromISO(bahnExpertTimestamp) 4 | export const toBahnExpertTimestamp = (timestamp: DateTime) => timestamp.toISO() 5 | export const fromSQLTimestamp = (sqlTimestamp: string) => DateTime.fromFormat(sqlTimestamp, 'yyyy-LL-dd HH:mm:ss', { zone: 'UTC' }) 6 | export const toSQLTimestamp = (timestamp: DateTime) => timestamp.setZone('UTC').toFormat('yyyy-LL-dd HH:mm:ss') 7 | export const bahnExpertToSQL = (bahnExpertTimestamp: string) => toSQLTimestamp(fromBahnExpertTimestamp(bahnExpertTimestamp)) -------------------------------------------------------------------------------- /src/fetcher/bahn_expert.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '../cache.js' 2 | import { ApiModule, request } from './request.js' 3 | 4 | const bahnExpertCache = new Cache('bahn_expert', 60) 5 | 6 | type BahnExpertStationPlace = { evaNumber: string, name: string } 7 | export type BahnExpertDeparture = { 8 | initialDeparture: string, 9 | arrival: { 10 | scheduledPlatform: string, 11 | platform: string, 12 | scheduledTime: string, 13 | time: string, 14 | delay: number, 15 | cancelled: boolean, 16 | hidden: boolean 17 | }, 18 | auslastung: boolean, 19 | currentStopPlace: { name: string, evaNumber: string }, 20 | departure: { 21 | scheduledPlatform: string, 22 | platform: string, 23 | scheduledTime: string, 24 | time: string, 25 | delay: number, 26 | cancelled: boolean, 27 | hidden: boolean 28 | }, 29 | destination: string, 30 | id: string, 31 | additionel: boolean, 32 | cancelled: boolean, 33 | mediumId: string, 34 | platform: string, 35 | rawId: string, 36 | ref: { trainNumber: string, trainType: string, train: string }, 37 | route: { additional: boolean, cancelled: boolean, showVia: boolean, name: string }[], 38 | scheduledDestination: string, 39 | scheduledPlatform: string, 40 | substitute: boolean, 41 | train: { 42 | name: string, 43 | line: string, 44 | number: string, 45 | type: string, 46 | admin: string, 47 | longDistance: boolean, 48 | operator: { name: string, icoX: number } 49 | } 50 | } 51 | type BahnExpertIrisAbfahrtenResponse = { departures: BahnExpertDeparture[], wings: { [key: string]: BahnExpertDeparture } } 52 | type BahnExpertCoachSequenceType = { 53 | stop: { stopPlace: { name: string, evaNumber: string } }, 54 | product: { number: string, type: string, line: string }, 55 | sequence: { groups: { 56 | name: string, 57 | originName: string, 58 | destinationName: string, 59 | trainName: string, 60 | number: string, 61 | baureihe: { // building_series 62 | name: string, 63 | baureihe: string, 64 | identifier: string 65 | }, 66 | coaches: { 67 | class: number, 68 | vehicleCategory: string, 69 | closed: boolean, 70 | uic: string, 71 | type: string, 72 | identificationNumber: string, 73 | }[] 74 | }[] }, 75 | direction: boolean 76 | } 77 | 78 | type BahnExpertDetailsType = { 79 | cancelled: boolean, 80 | changeDuration: number, 81 | finalDestination: string, 82 | jid: string, 83 | product: { 84 | name: string, 85 | number: string, 86 | icoX: number, 87 | cls: number, 88 | oprX: number, 89 | addName: string, 90 | nameS: string, 91 | matchId: string 92 | }, 93 | stops: { 94 | arrival: { scheduledPlatform: string, platform: string, scheduledTime: string, time: string, delay: number, cancalled: boolean }, 95 | departure: { scheduledPlatform: string, platform: string, scheduledTime: string, time: string, delay: number, cancalled: boolean }, 96 | station: { name: string, evaNumber: string }, 97 | auslastung: { first: number, second: string }, 98 | additional: boolean, 99 | cancelled: boolean 100 | }[] 101 | } 102 | 103 | export const getStationByEva = async (evaNumber: number): Promise => 104 | await request(ApiModule.BAHN_EXPERT, '/stopPlace/v1/[evaNumber]', { evaNumber: String(evaNumber) }, { ignoreStatusCodes: [ 404 ], cache: bahnExpertCache, cacheTTL: 60 * 60 * 24 * 30 }) 105 | 106 | 107 | export const getEvaByStation = async (station: string): Promise => { 108 | const response = await request(ApiModule.BAHN_EXPERT, '/stopPlace/v1/search/[station]', { station, max: '1' }, { useGetArguments: ['max'], ignoreStatusCodes: [ 404 ], cache: bahnExpertCache, cacheTTL: 60 * 60 * 24 * 30 }) 109 | if (response && response.length > 0) 110 | return response[0] 111 | } 112 | 113 | export const getIRISDepartures = async (evaNumber: number, lookahead?: number, lookbehind?: number): Promise => 114 | await request(ApiModule.BAHN_EXPERT, '/iris/v2/abfahrten/[evaNumber]', { evaNumber: String(evaNumber), lookahead: lookahead ? String(lookahead) : null, lookbehind: lookbehind ? String(lookbehind) : null }, { ignoreStatusCodes: [404], cache: bahnExpertCache, cacheTTL: 60 * 10, useGetArguments: ['lookahead', 'lookbehind'] }) 115 | 116 | export const getCoachSequence = async (trainNumber: number, trainCategory: string, initialDeparture: string, departure: string, evaNumber: number): Promise => 117 | await request(ApiModule.BAHN_EXPERT, '/coachSequence/v4/wagen/[trainNumber]', { trainNumber: String(trainNumber), category: trainCategory, initialDeparture, departure, evaNumber: String(evaNumber) }, { ignoreStatusCodes: [404], cache: bahnExpertCache, cacheTTL: 60 * 5, useGetArguments: ['initialDeparture', 'departure', 'evaNumber', 'category']}) 118 | 119 | export const getTrainDetails = async (trainName: string, station: number, date: string): Promise => 120 | await request(ApiModule.BAHN_EXPERT, '/hafas/v2/details/[trainName]', { trainName, station: String(station), date }, { cache: bahnExpertCache, cacheTTL: 60 * 3, useGetArguments: [ 'station', 'date'], ignoreStatusCodes: [404]}) -------------------------------------------------------------------------------- /src/fetcher/request.ts: -------------------------------------------------------------------------------- 1 | import fetch, { Response } from 'node-fetch' 2 | import { Cache } from '../cache.js' 3 | import { debug, error } from '../logger.js' 4 | import { incrementMetric } from '../metrics.js' 5 | 6 | export const ApiModule = { 7 | BAHN_EXPERT: process.env.BAHN_EXPERT_API_PATH || "https://bahn.expert/api" 8 | } 9 | 10 | type ApiModuleType = string 11 | 12 | const checkRequest = async (path: string, response: Response, ignoreStatusCodes?: number[]): Promise => { 13 | if (ignoreStatusCodes?.includes(response.status)) { 14 | return null 15 | } 16 | if (response.status !== 200 && response.status !== 204) { 17 | throw new Error(`Error while fetching ${path}: ${response.statusText}: ${await response.text()}`) 18 | } 19 | 20 | return await response.json() 21 | } 22 | 23 | export const request = async ( 24 | apiModule: ApiModuleType, 25 | path: string, 26 | args: { [key: string]: string | null }, 27 | options?: { ignoreStatusCodes?: number[], cache?: Cache, cacheTTL?: number, useGetArguments?: string[] }, 28 | maxRetries: number=3, retryCount: number=0): Promise => { 29 | let metric_path = `request_${path}` 30 | for (const [argName, argValue] of Object.entries(args)) { 31 | if (path.includes(`[${argName}]`) && argValue) { 32 | path = path.replace(`[${argName}]`, argValue) 33 | } 34 | } 35 | if (options?.useGetArguments && options.useGetArguments.length > 0) { 36 | const getArguments: { [key: string]: string } = {} 37 | for (const getArgument of options.useGetArguments) { 38 | if (args[getArgument]) { 39 | getArguments[getArgument] = args[getArgument]! 40 | } 41 | } 42 | if (Object.keys(getArguments).length > 0) 43 | path = `${path}?${new URLSearchParams(getArguments)}` 44 | } 45 | path = `${apiModule}${path}` 46 | if (options?.cache) { 47 | const cachedResponse = await options.cache.get(path) 48 | if (cachedResponse) { 49 | metric_path += "{cached=true}" 50 | incrementMetric(metric_path) 51 | return cachedResponse 52 | } 53 | } 54 | metric_path += "{cached=false}" 55 | incrementMetric(metric_path) 56 | try { 57 | const controller = new AbortController() 58 | setTimeout(() => controller.abort(), 1000 * 20) 59 | const fetch_response = await fetch(path, { signal: controller.signal }) 60 | const response = await checkRequest(path, fetch_response, options?.ignoreStatusCodes) 61 | if (response && options?.cache) { 62 | options.cache.set(path, response, options.cacheTTL).catch(e => error(`Error while caching ${path}: ${e}`)) 63 | } 64 | return response 65 | } catch (e) { 66 | if (retryCount < maxRetries) { 67 | debug(`Error ${e} while request to ${path}, retry ${retryCount}/${maxRetries}`) 68 | return await request(apiModule, path, args, options, maxRetries, retryCount + 1) 69 | } else { 70 | throw e 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/heartbeat.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import fetch from 'node-fetch' 3 | import database from './database.js' 4 | 5 | const runHeartbeat = async () => { 6 | let heartbeatUrl = process.env.HEARTBEAT_URL 7 | const newestTrainTrip = await database('train_trip').whereNotNull('destination_station').orderBy('id', 'desc').first() 8 | 9 | if (newestTrainTrip) { 10 | const newestTrainTripHour = DateTime.now().diff(DateTime.fromJSDate(newestTrainTrip.timestamp)).as('hours') 11 | if (newestTrainTripHour > 6) { 12 | console.error(`The newest train trip in the database is about 6 hours (${newestTrainTripHour} hours) old. It seem's like the backend is broken. Try to restart.`) 13 | console.error('Heartbeat disabled because the backend seems to broken') 14 | heartbeatUrl = undefined // disable heartbeat 15 | } 16 | } else { 17 | console.warn('No full train trip does yet exist in the database') 18 | } 19 | 20 | if (heartbeatUrl) { 21 | fetch(heartbeatUrl) 22 | } 23 | } 24 | 25 | export default runHeartbeat -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { autoFetch } from './autoFetch.js' 2 | import './rabbit/rabbit.js' 3 | import Sentry from '@sentry/node' 4 | import { startWebserver } from './metrics.js' 5 | 6 | if (process.env.SENTRY_DSN) { 7 | Sentry.init({ dsn: process.env.SENTRY_DSN }) 8 | Sentry.setTag('environment', process.env.ENVIRONMENT || 'local') 9 | } 10 | 11 | autoFetch() 12 | startWebserver() -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import {Shoutrrr } from 'shoutrrrd-js' 2 | import dotenv from 'dotenv' 3 | import { DateTime } from 'luxon' 4 | 5 | dotenv.config() 6 | 7 | export const LogLevels: { [level: string]: number } = { 8 | DEBUG: 0, 9 | INFO: 1, 10 | WARN: 2, 11 | ERROR: 3, 12 | CRITICAL: 4, 13 | } 14 | const minimalShoutrrrLogLevel = LogLevels[(process.env.SHOUTRRR_MIN_LOGLEVEL || 'WARN').toUpperCase()] 15 | const minimalConsoleLogLevel = LogLevels[(process.env.MINIMAL_CONSOLE_LOG_LEVEL || 'INFO').toUpperCase()] 16 | 17 | 18 | const shoutrrr = process.env.SHOUTRRR_API_URL && process.env.SHOUTRRR_SERVICE_NAME ? new Shoutrrr(process.env.SHOUTRRR_API_URL) : null 19 | 20 | let maxLogLevelLength = 0 21 | Object.keys(LogLevels).forEach(e => e.length > maxLogLevelLength ? maxLogLevelLength = e.length : null) 22 | 23 | const logShoutrrr = async (content: string) => { 24 | if (shoutrrr) { 25 | await shoutrrr.send(process.env.SHOUTRRR_SERVICE_NAME!, content) 26 | } 27 | } 28 | 29 | export const log = (level: string, message: string) => { 30 | level = level.toUpperCase() 31 | if (!Object.keys(LogLevels).includes(level)) { 32 | throw new Error(`LogLevel ${level} doesn't exists, so log message could not be sent.`) 33 | } 34 | const levelNumber = LogLevels[level] 35 | const sendingContent = `${level.toUpperCase().padEnd(maxLogLevelLength)} [${DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss')}] ${message}` 36 | if (levelNumber >= minimalConsoleLogLevel) console.log(sendingContent) 37 | if (levelNumber >= minimalShoutrrrLogLevel) logShoutrrr(sendingContent).catch(error => console.error(`Error ${error} happend while sending message to shoutrrr.`)) 38 | } 39 | 40 | export const debug = (message: string) => log('DEBUG', message) 41 | export const info = (message: string) => log('INFO', message) 42 | export const warn = (message: string) => log('WARN', message) 43 | export const error = (message: string) => log('ERROR', message) 44 | export const critical = (message: string) => log('CRITICAL', message) -------------------------------------------------------------------------------- /src/metrics.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import express from 'express' 3 | import { info } from './logger.js' 4 | 5 | const app = express() 6 | 7 | type Metric = { [key: string]: number } 8 | var metrics: { [key: number]: Metric } = {} 9 | 10 | const METRICS_KEY_PREFIX = process.env.METRICS_KEY_PREFIX || 'regenbogen_ice_' 11 | 12 | export const incrementMetric = (key: string) => { 13 | const hour = new Date().getHours() 14 | if (!metrics[hour]) { 15 | metrics[hour] = {} 16 | } 17 | if (!metrics[hour][METRICS_KEY_PREFIX + key]) { 18 | metrics[hour][METRICS_KEY_PREFIX + key] = 0 19 | } 20 | metrics[hour][METRICS_KEY_PREFIX + key] += 1 21 | } 22 | 23 | export const getMetrics = (): Metric => { 24 | const hour = new Date().getHours() 25 | const currentMetric = metrics[hour] 26 | metrics = {} 27 | metrics[hour] = currentMetric 28 | return currentMetric 29 | } 30 | 31 | app.get('/metrics', (req, res) => { 32 | let body = "" 33 | let metrics = getMetrics() 34 | for (let k in metrics) { 35 | body += k + " " 36 | body += metrics[k].toString() + "\n" 37 | } 38 | res.setHeader('content-type', 'text/plain') 39 | res.send(body) 40 | }) 41 | 42 | export const startWebserver = () => { 43 | const HTTP_HOST = process.env.METRIC_HTTP_HOST || '::1' 44 | const HTTP_PORT = parseInt(process.env.METRIC_HTTP_PORT || '3030') 45 | app.listen(HTTP_PORT, HTTP_HOST, () => { 46 | info(`Metric HTTP server is running on http://${HTTP_HOST}:${HTTP_PORT}`) 47 | }) 48 | } -------------------------------------------------------------------------------- /src/rabbit/fetch_coach_sequence.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import database from '../database.js' 3 | import { toSQLTimestamp } from '../dateTimeFormat.js' 4 | import { getCoachSequence, getEvaByStation } from '../fetcher/bahn_expert.js' 5 | import { debug } from '../logger.js' 6 | import rabbitAsyncHandler from '../rabbitAsyncHandler.js' 7 | 8 | type FetchCoachSequence = { trainId: number, trainNumber: number, trainType: string, evaDeparture: string, evaNumber: number, initialDeparture: string } 9 | 10 | 11 | const getTrainVehicle = async ( 12 | train_vehicle_number: number, 13 | train_vehicle_name: string, 14 | train_type: string, 15 | building_series: { building_series: number | null, building_series_name: string } | null 16 | ): Promise => { 17 | let trainVehicleId = (await database('train_vehicle').insert({ 18 | train_vehicle_number, 19 | train_vehicle_name, 20 | train_type, 21 | building_series: building_series?.building_series, 22 | building_series_name: building_series?.building_series_name 23 | }).onConflict('train_vehicle_number').merge())[0] 24 | if (trainVehicleId) { 25 | return trainVehicleId 26 | } else { 27 | return (await database('train_vehicle').where({ train_vehicle_number }).select(['id']).first())["id"] 28 | } 29 | } 30 | 31 | const checkCoachIntegrity = async (trainVehicleId: number | null, coaches: { uic: string, vehicleCategory: string, class: number, type: string }[]): Promise => { 32 | const coachSequence = trainVehicleId ? await database('coach_sequence').where({ train_vehicle_id: trainVehicleId }).orderBy('timestamp', 'desc').select('id').first() : null 33 | if (!coachSequence && trainVehicleId) return false 34 | const coachSequenceId = coachSequence ? coachSequence.id : null 35 | for (const [coachIndex, coach] of coaches.entries()) { 36 | let sql = database('coach').where({ 37 | coach_sequence_id: coachSequenceId, 38 | uic: coach.uic, 39 | category: coach.vehicleCategory, 40 | class: coach.class, 41 | type: coach.type 42 | }) 43 | if (trainVehicleId) { 44 | sql = sql.andWhere((builder) => { 45 | builder.where({ index: coachIndex }).orWhere({ index: coaches.length - coachIndex - 1 }) 46 | }) 47 | } 48 | const databaseCoach = await sql.first() 49 | if (!databaseCoach) return false 50 | } 51 | return true 52 | } 53 | 54 | const createCoaches = async (trainVehicleId: number | null, coaches: { uic: string, vehicleCategory: string, class: number, type: string }[]) => { 55 | let coachSequenceId = null 56 | if (trainVehicleId) { 57 | const coachSequence = await database('coach_sequence').insert({ train_vehicle_id: trainVehicleId }) 58 | coachSequenceId = coachSequence[0] 59 | } 60 | for (const [coachIndex, coach] of coaches.entries()) { 61 | debug(`Create coach ${coach.uic} in sequence ${coachSequenceId}.`) 62 | await database('coach').insert({ 63 | coach_sequence_id: coachSequenceId, 64 | index: trainVehicleId ? coachIndex : null, 65 | uic: coach.uic, 66 | category: coach.vehicleCategory, 67 | class: coach.class, 68 | type: coach.type 69 | }) 70 | } 71 | } 72 | 73 | const createTrainTripVehicle = async (trainId: number, groupIndex: number, trainVehicleId: number, origin: number | null, destination: number | null) => { 74 | const oldTrainTripVehicle = await database('train_trip_vehicle').where({ train_trip_id: trainId, group_index: groupIndex }).select(['id', 'train_vehicle_id', 'timestamp', 'origin', 'destination']).first() 75 | if (oldTrainTripVehicle && oldTrainTripVehicle.train_vehicle_id != trainVehicleId) { 76 | debug(`Train vehicle changed from ${oldTrainTripVehicle.train_vehicle_id}.`) 77 | await database('train_trip_vehicle').where({ id: oldTrainTripVehicle.id }).delete() 78 | await database('train_trip_vehicle_change').insert({ train_trip_id: trainId, train_vehicle_id: oldTrainTripVehicle.train_vehicle_id, group_index: groupIndex, original_timestamp: oldTrainTripVehicle.timestamp, origin, destination }) 79 | } else if (oldTrainTripVehicle && (oldTrainTripVehicle.origin != origin || oldTrainTripVehicle.destination != destination)) { 80 | debug(`Origin or destination on train_trip_vehicle ${oldTrainTripVehicle.id} changed. Updating.`) 81 | await database('train_trip_vehicle').where({ id: oldTrainTripVehicle.id }).update({ 82 | origin, 83 | destination 84 | }) 85 | return 86 | } else if (oldTrainTripVehicle) 87 | return 88 | await database('train_trip_vehicle').insert({ 89 | train_trip_id: trainId, 90 | group_index: groupIndex, 91 | train_vehicle_id: trainVehicleId, 92 | origin, 93 | destination 94 | }) 95 | } 96 | 97 | export const fetch_coach_sequence = rabbitAsyncHandler(async (msg: FetchCoachSequence) => { 98 | debug(`Starting to fetch coach sequence for ${msg.trainType}${msg.trainNumber} (ID ${msg.trainId})`) 99 | const coachSequence = await getCoachSequence(msg.trainNumber, msg.trainType, msg.initialDeparture, msg.evaDeparture, msg.evaNumber) 100 | if (!coachSequence) return 101 | 102 | const vehicleGroups = coachSequence.sequence.groups.filter(e => +e.number == msg.trainNumber) 103 | 104 | for (const [originalGroupIndex, coaches] of vehicleGroups.entries()) { 105 | if (+coaches.number != msg.trainNumber || vehicleGroups === null || coaches.coaches === null) continue 106 | const groupIndex = coachSequence.direction ? originalGroupIndex : vehicleGroups.length -originalGroupIndex - 1 107 | if (coaches.name.includes('planned') || (!coaches.baureihe && msg.trainType !== 'IC')) { 108 | debug(`${msg.trainType}${msg.trainNumber}[${groupIndex}]: Vehicle is planned because coaches name is ${coaches.name}.`) 109 | continue 110 | } 111 | const IC_1 = !(coaches.name.includes('ICE') || coaches.name.includes('ICK') || coaches.name.includes('ICD')) 112 | if (coaches.name.includes('planned') || coaches.name.length < 3) { 113 | debug(`Train ${msg.trainId}[${groupIndex}] (${msg.trainType}) seems to be a non fetchable train.`) 114 | continue 115 | } 116 | let trainVehicleId: number | null = null 117 | if (!IC_1) { 118 | const trainType = coaches.name.slice(0,3).toUpperCase() 119 | const trainVehicleNumber = +coaches.name.slice(3) 120 | if (!trainVehicleNumber || !trainType) { 121 | debug(`Train ${msg.trainId}[${groupIndex}] (${msg.trainType}) seems to be a train without product group.`) 122 | continue 123 | } 124 | trainVehicleId = await getTrainVehicle( 125 | trainVehicleNumber, 126 | coaches.trainName, 127 | trainType, 128 | coaches.baureihe ? { 129 | building_series: coaches.baureihe.baureihe ? +coaches.baureihe.baureihe : null, 130 | building_series_name: coaches.baureihe.name 131 | } : null 132 | ) 133 | } 134 | if (!(await checkCoachIntegrity(trainVehicleId, coaches.coaches))) { 135 | debug(`No coach integrity. Creating coaches.`) 136 | await createCoaches(trainVehicleId, coaches.coaches) 137 | await database('train_trip_coaches_identification').where({ train_trip_id: msg.trainId }).delete() 138 | for (const coach of coaches.coaches) { 139 | if (coach.uic) 140 | debug(`Created coach_identification for ${coach.uic} on identification_number ${coach.identificationNumber}.`) 141 | await database.raw('INSERT INTO train_trip_coaches_identification (train_trip_id, coach_id, identification_number) SELECT ?, coach.id, ? FROM coach WHERE uic = ?', [msg.trainId, coach.identificationNumber || null, coach.uic]) 142 | } 143 | } 144 | const originStation = coaches.originName ? await getEvaByStation(coaches.originName) : null 145 | const origin = originStation ? +originStation.evaNumber : null 146 | const destinationStation = coaches.destinationName ? await getEvaByStation(coaches.destinationName) : null 147 | const destination = destinationStation ? +destinationStation.evaNumber : null 148 | if (trainVehicleId) { 149 | await createTrainTripVehicle(msg.trainId, groupIndex, trainVehicleId, origin, destination) 150 | } 151 | } 152 | await database('train_trip').where({ id: msg.trainId }).update({ coach_sequence_update_expire: toSQLTimestamp(DateTime.now().plus({ hours: 1 })) }) 153 | }) -------------------------------------------------------------------------------- /src/rabbit/fetch_train_details.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import database from '../database.js' 3 | import { bahnExpertToSQL, toSQLTimestamp } from '../dateTimeFormat.js' 4 | import { getTrainDetails } from '../fetcher/bahn_expert.js' 5 | import { debug } from '../logger.js' 6 | import rabbitAsyncHandler from '../rabbitAsyncHandler.js' 7 | 8 | type FetchTrainDetails = { trainId: number, trainNumber: number, trainType: number, initialDeparture: string, evaNumber: number } 9 | 10 | export const fetch_train_details = rabbitAsyncHandler(async (msg: FetchTrainDetails) => { 11 | const trainDetails = await getTrainDetails(msg.trainType + String(msg.trainNumber), msg.evaNumber, msg.initialDeparture) 12 | if (!trainDetails) return 13 | if (trainDetails.cancelled) { 14 | debug(`${msg.trainId} was cancelled.`) 15 | await database('train_trip').where({ id: msg.trainId }).delete() 16 | return 17 | } 18 | const stops = trainDetails.stops.map((stop, index) => { 19 | return { 20 | train_trip_id: msg.trainId, 21 | index, 22 | cancelled: stop.cancelled, 23 | station: +stop.station.evaNumber, 24 | scheduled_departure: stop.departure ? bahnExpertToSQL(stop.departure.scheduledTime) : null, 25 | departure: stop.departure ? bahnExpertToSQL(stop.departure.time) : null, 26 | departure_delay: stop.departure ? stop.departure.delay : null, 27 | scheduled_arrival: stop.arrival ? bahnExpertToSQL(stop.arrival.scheduledTime) : null, 28 | arrival: stop.arrival ? bahnExpertToSQL(stop.arrival.time) : null, 29 | arrival_delay: stop.arrival ? stop.arrival.delay : null, 30 | } 31 | }) 32 | 33 | await database('train_trip').where({ id: msg.trainId }).update({ origin_station: stops[0].station, destination_station: stops[stops.length - 1].station}) 34 | 35 | let trainTripRoutes = await database('train_trip_route').where({ train_trip_id: msg.trainId }).orderBy('index', 'asc').select('*') 36 | if (trainTripRoutes.length !== 0 && trainTripRoutes.length != stops.length) { 37 | debug(`Saved trip route of ${msg.trainId} length is invalid.`) 38 | await database('train_trip_route').where({ train_trip_id: msg.trainId }).delete() 39 | trainTripRoutes = [] 40 | } 41 | 42 | if (trainTripRoutes.length === 0) { 43 | debug(`Trip route for ${msg.trainId} saved.`) 44 | await database('train_trip_route').insert(stops) 45 | } else { 46 | for (const stop of trainTripRoutes) { 47 | const newStop = stops[stop.index] 48 | if ( 49 | stop.station != newStop.station || 50 | stop.scheduled_departure != newStop.scheduled_departure || 51 | stop.scheduled_arrival != newStop.scheduled_departure 52 | ) { 53 | debug(`Trip route for ${msg.trainId} saved because old list in invalid.`) 54 | await database('train_trip_route').where({ train_trip_id: msg.trainId }).delete() 55 | await database('train_trip_route').insert(stops) 56 | break 57 | } 58 | if ( 59 | stop.cancelled != newStop.cancelled || 60 | stop.departure != newStop.departure || 61 | stop.arrival != newStop.arrival 62 | ) { 63 | debug(`Trip route for ${msg.trainId} updated (index ${stop.index}).`) 64 | await database('train_trip_route').where({ id: stop.id }).update({ 65 | cancelled: newStop.cancelled, 66 | departure: newStop.departure, 67 | departure_delay: newStop.departure_delay, 68 | arrival: newStop.arrival, 69 | arrival_delay: newStop.arrival_delay, 70 | updated: toSQLTimestamp(DateTime.now()) 71 | }) 72 | } 73 | } 74 | } 75 | await database('train_trip').where({ id: msg.trainId }).update({ routes_update_expire: toSQLTimestamp(DateTime.now().plus({ minutes: 20 })) }) 76 | 77 | }) 78 | -------------------------------------------------------------------------------- /src/rabbit/fetch_train_numbers.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | import database from '../database.js' 3 | import { bahnExpertToSQL } from '../dateTimeFormat.js' 4 | import { getIRISDepartures } from '../fetcher/bahn_expert.js' 5 | import { debug, error } from '../logger.js' 6 | import rabbitAsyncHandler from '../rabbitAsyncHandler.js' 7 | import staticConfig from '../staticConfig.js' 8 | import { rabbit } from './rabbit.js' 9 | 10 | type FetchTrainNumbersData = { evaNumbers: number[] } 11 | export const fetch_train_numbers = rabbitAsyncHandler(async (msg: FetchTrainNumbersData) => { 12 | const alreadyFetched: string[] = [] 13 | for (const evaNumber of msg.evaNumbers) { 14 | const departuresResponse = await getIRISDepartures(evaNumber, staticConfig.IRIS_LOOKAHEAD) 15 | if (!departuresResponse) { 16 | error(`getIRISDepartures for evaNumber ${evaNumber} failed. The function returned null.`) 17 | continue 18 | } 19 | const allDepartures = departuresResponse.departures 20 | allDepartures.concat(Object.values(departuresResponse.wings)) 21 | const departures = allDepartures.filter(e => 22 | staticConfig.FETCHABLE_TRAIN_TYPES.includes(e.train.type) && !e.substitute && e.departure && !alreadyFetched.includes(e.train.name)) 23 | debug(`Fetched ${departures.length} train_trips on evaNumber ${evaNumber}.`) 24 | for(const train of departures) { 25 | alreadyFetched.push(train.train.name) 26 | if (train.cancelled) { 27 | debug(`${train.train.name} was cancelled.`) 28 | await database('train_trip').where({ 29 | train_type: train.train.type, 30 | train_number: +train.train.number, 31 | initial_departure: bahnExpertToSQL(train.initialDeparture) 32 | }).delete() 33 | continue 34 | } 35 | const existingTrain = await database('train_trip').where({ 36 | train_type: train.train.type, 37 | train_number: +train.train.number, 38 | initial_departure: bahnExpertToSQL(train.initialDeparture) 39 | }).select(['id', 'routes_update_expire', 'coach_sequence_update_expire']) 40 | let trainId = null 41 | let fetch_coaches = true 42 | let fetch_details = true 43 | if (existingTrain.length > 0) { 44 | trainId = existingTrain[0].id 45 | debug(`Train_trip ${trainId} already exists.`) 46 | if (existingTrain[0].coach_sequence_update_expire && DateTime.fromJSDate(existingTrain[0].coach_sequence_update_expire) > DateTime.now()) { 47 | fetch_coaches = false 48 | } 49 | if (existingTrain[0].routes_update_expire && DateTime.fromJSDate(existingTrain[0].routes_update_expire) > DateTime.now()) { 50 | fetch_details = false 51 | } 52 | } else { 53 | const databaseTrain = await database('train_trip').insert({ 54 | train_type: train.train.type, 55 | train_number: +train.train.number, 56 | initial_departure: bahnExpertToSQL(train.initialDeparture) 57 | }) 58 | trainId = databaseTrain[0] 59 | debug(`Created train_trip ${trainId}.`) 60 | } 61 | if (fetch_coaches) 62 | await rabbit.publish('fetch_coach_sequence', { 63 | trainId, 64 | trainNumber: +train.train.number, 65 | trainType: train.train.type, 66 | initialDeparture: train.initialDeparture, 67 | evaDeparture: train.departure.scheduledTime, 68 | evaNumber 69 | }) 70 | if (fetch_details) 71 | await rabbit.publish('fetch_train_details', { 72 | trainId, 73 | trainNumber: +train.train.number, 74 | trainType: train.train.type, 75 | initialDeparture: train.initialDeparture, 76 | evaNumber 77 | }) 78 | } 79 | } 80 | }) -------------------------------------------------------------------------------- /src/rabbit/rabbit.ts: -------------------------------------------------------------------------------- 1 | import { Rabbit } from 'rabbit-queue' 2 | import { error, info, log } from '../logger.js' 3 | import { fetch_coach_sequence } from './fetch_coach_sequence.js' 4 | import { fetch_train_details } from './fetch_train_details.js' 5 | import { fetch_train_numbers } from './fetch_train_numbers.js' 6 | 7 | export const rabbit = new Rabbit(process.env.RABBIT_URL || 'amqp://localhost') 8 | 9 | 10 | rabbit.on('connected', () => { 11 | info(`Rabbit connected.`) 12 | }) 13 | 14 | rabbit.on('disconnect', (err = new Error(`Rabbitmq disconnected.`)) => { 15 | error(`Disconnected from rabbit: ${err}. Trying to reconnect.`) 16 | setTimeout(() => rabbit.reconnect(), 100) 17 | }) 18 | 19 | rabbit.on('log', (component, level, ...args) => { 20 | if (process.env.ENABLE_RABBIT_DEBUG_LOGS != 'true' && level.toLowerCase() == 'debug') 21 | return 22 | if (level != 'trace') log(level, `Rabbit: ${component} ${args.join(' ')}`) 23 | }) 24 | 25 | if (process.env.PURGE_RABBIT_QUEUES_ON_STARTUP == 'true') { 26 | await rabbit.destroyQueue('fetch_train_numbers') 27 | await rabbit.destroyQueue('fetch_coach_sequence') 28 | await rabbit.destroyQueue('fetch_train_details') 29 | info(`Purged rabbit queues.`) 30 | } 31 | 32 | await rabbit.createQueue('fetch_train_numbers', { "x-max-length": 500 }, fetch_train_numbers) 33 | await rabbit.createQueue('fetch_coach_sequence', { "x-max-length": 2000 }, fetch_coach_sequence) 34 | await rabbit.createQueue('fetch_train_details', { "x-max-length": 2000 }, fetch_train_details) 35 | info(`Created rabbit queues.`) -------------------------------------------------------------------------------- /src/rabbitAsyncHandler.ts: -------------------------------------------------------------------------------- 1 | import { error } from './logger.js' 2 | import Sentry from '@sentry/node' 3 | 4 | type RabbitCallback = (msg: any, ack: (error?: any, reply?: any) => void) => void 5 | type InternalRabbitCallback = (msg: any) => Promise<{ error?: any, reply?: any }|void> 6 | 7 | const rabbitAsyncHandler = (callback: InternalRabbitCallback): RabbitCallback => { 8 | return (msg, ack) => { 9 | try { 10 | callback(JSON.parse(msg.content.toString())).then(e => { 11 | if (e) ack(e.error, e.reply) 12 | else ack() 13 | }).catch(e => { 14 | ack({ error: e.toString() }) 15 | error(`Error while handling (callback) rabbit message: ${msg.fields.routingKey}: ${msg.content.toString()} ${e}\n${e.stack}.`) 16 | if (process.env.SENTRY_DSN) { 17 | Sentry.configureScope(scope => { 18 | scope.setExtra('handler', msg.fields.routingKey) 19 | scope.setExtra('content', JSON.parse(msg.content.toString())) 20 | }) 21 | Sentry.captureException(e) 22 | } 23 | }) 24 | } catch (e: any) { 25 | ack({ error: e.toString() }) 26 | error(`Error while handling rabbit message: ${msg.fields.routingKey}: ${msg.content.toString()}: ${e}\n${e.stack}.`) 27 | if (process.env.SENTRY_DSN) { 28 | Sentry.configureScope(scope => { 29 | scope.setExtra('handler', msg.fields.routingKey) 30 | scope.setExtra('content', JSON.parse(msg.content.toString())) 31 | }) 32 | Sentry.captureException(e) 33 | } 34 | } 35 | } 36 | } 37 | 38 | export default rabbitAsyncHandler -------------------------------------------------------------------------------- /src/redis.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | import { debug, error } from './logger.js' 3 | 4 | const client = createClient({ 5 | url: process.env.REDIS_URL 6 | }) 7 | 8 | client.on('error', (err) => error(`Redis error: ${err}`)) 9 | 10 | await client.connect() 11 | debug(`Redis ${process.env.REDIS_URL} connected.`) 12 | 13 | 14 | export default client -------------------------------------------------------------------------------- /src/staticConfig.ts: -------------------------------------------------------------------------------- 1 | import { BahnExpertDeparture } from "./fetcher/bahn_expert.js" 2 | 3 | type StaticConfig = { 4 | IRIS_LOOKAHEAD: number, 5 | FETCHABLE_TRAIN_TYPES: Array, 6 | AUTO_FETCH_EVA_NUMBERS: Array 7 | } 8 | const staticConfig: StaticConfig = { 9 | IRIS_LOOKAHEAD: 60 * 24, 10 | FETCHABLE_TRAIN_TYPES: ['ICE', 'IC'], 11 | 12 | AUTO_FETCH_EVA_NUMBERS: [ 13 | 8002549, // Hamburg 14 | 8000050, // Bremen 15 | 8000152, // Hannover 16 | 8000036, // Bielefeld 17 | 8000149, // Hamm 18 | 8000098, // Essen 19 | 8000207, // Köln 20 | 8000105, // Frankfurt Hbf 21 | 8070003, // Frankfurt Flughafen 22 | 8000244, // Mannheim 23 | 8000191, // Karlsruhe 24 | 8000096, // Stuttgart 25 | 8000261, // München 26 | 8010101, // Erfurt 27 | 8003200, // Kassel-Wilhelmshöhe 28 | 8010159, // Halle 29 | 8010205, // Leipzig 30 | 8010085, // Dresden 31 | 8011160, // Berlin 32 | ] 33 | } 34 | 35 | export default staticConfig -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2022", /* Specify what module code is generated. */ 28 | "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@node-redis/bloom@1.0.1": 6 | version "1.0.1" 7 | resolved "https://registry.yarnpkg.com/@node-redis/bloom/-/bloom-1.0.1.tgz#144474a0b7dc4a4b91badea2cfa9538ce0a1854e" 8 | integrity sha512-mXEBvEIgF4tUzdIN89LiYsbi6//EdpFA7L8M+DHCvePXg+bfHWi+ct5VI6nHUFQE5+ohm/9wmgihCH3HSkeKsw== 9 | 10 | "@node-redis/client@1.0.4": 11 | version "1.0.4" 12 | resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.4.tgz#fe185750df3bcc07524f63fe8dbc8d14d22d6cbb" 13 | integrity sha512-IM/NRAqg7MvNC3bIRQipXGrEarunrdgvrbAzsd3ty93LSHi/M+ybQulOERQi8a3M+P5BL8HenwXjiIoKm6ml2g== 14 | dependencies: 15 | cluster-key-slot "1.1.0" 16 | generic-pool "3.8.2" 17 | redis-parser "3.0.0" 18 | yallist "4.0.0" 19 | 20 | "@node-redis/graph@1.0.0": 21 | version "1.0.0" 22 | resolved "https://registry.yarnpkg.com/@node-redis/graph/-/graph-1.0.0.tgz#baf8eaac4a400f86ea04d65ec3d65715fd7951ab" 23 | integrity sha512-mRSo8jEGC0cf+Rm7q8mWMKKKqkn6EAnA9IA2S3JvUv/gaWW/73vil7GLNwion2ihTptAm05I9LkepzfIXUKX5g== 24 | 25 | "@node-redis/json@1.0.2": 26 | version "1.0.2" 27 | resolved "https://registry.yarnpkg.com/@node-redis/json/-/json-1.0.2.tgz#8ad2d0f026698dc1a4238cc3d1eb099a3bee5ab8" 28 | integrity sha512-qVRgn8WfG46QQ08CghSbY4VhHFgaTY71WjpwRBGEuqGPfWwfRcIf3OqSpR7Q/45X+v3xd8mvYjywqh0wqJ8T+g== 29 | 30 | "@node-redis/search@1.0.3": 31 | version "1.0.3" 32 | resolved "https://registry.yarnpkg.com/@node-redis/search/-/search-1.0.3.tgz#7c3d026bf994caf82019fd0c3924cfc09f041a29" 33 | integrity sha512-rsrzkGWI84di/uYtEctS/4qLusWt0DESx/psjfB0TFpORDhe7JfC0h8ary+eHulTksumor244bXLRSqQXbFJmw== 34 | 35 | "@node-redis/time-series@1.0.2": 36 | version "1.0.2" 37 | resolved "https://registry.yarnpkg.com/@node-redis/time-series/-/time-series-1.0.2.tgz#5dd3638374edd85ebe0aa6b0e87addc88fb9df69" 38 | integrity sha512-HGQ8YooJ8Mx7l28tD7XjtB3ImLEjlUxG1wC1PAjxu6hPJqjPshUZxAICzDqDjtIbhDTf48WXXUcx8TQJB1XTKA== 39 | 40 | "@sentry/core@7.9.0": 41 | version "7.9.0" 42 | resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.9.0.tgz#fb9308e067a4b5794eb49f8bac303bb9c627b1a9" 43 | integrity sha512-WVGd2hV7Clcpl7/GL8LsRr4Zk9o/7o4rZHfs1Qed5lMRNYcxiMwucC1CYILVpJqVfY+8vIRP9v9Ss9ta2VUikw== 44 | dependencies: 45 | "@sentry/hub" "7.9.0" 46 | "@sentry/types" "7.9.0" 47 | "@sentry/utils" "7.9.0" 48 | tslib "^1.9.3" 49 | 50 | "@sentry/hub@7.9.0": 51 | version "7.9.0" 52 | resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.9.0.tgz#29d4c796006282a20e5f78a1bb156d8f5eacd772" 53 | integrity sha512-KzPbGCB5mONgsXEzqHy6uOaOuqLnhmFmSpGg+M03J6UJnJaNM7nrNp80MhStmjLMq6whEYVE07DrMAn3+iaQdg== 54 | dependencies: 55 | "@sentry/types" "7.9.0" 56 | "@sentry/utils" "7.9.0" 57 | tslib "^1.9.3" 58 | 59 | "@sentry/node@^7.9.0": 60 | version "7.9.0" 61 | resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.9.0.tgz#2ce1f9336a22cee3530ccd472081f71073af5116" 62 | integrity sha512-MeggSCLyhUhsX3gRRvDhTADKYshuWjTRO/dUv3Jw+2xToSDvSWXJXDkIg5mCdlyOhh9/G+5xdWY58CfomzPZgg== 63 | dependencies: 64 | "@sentry/core" "7.9.0" 65 | "@sentry/hub" "7.9.0" 66 | "@sentry/types" "7.9.0" 67 | "@sentry/utils" "7.9.0" 68 | cookie "^0.4.1" 69 | https-proxy-agent "^5.0.0" 70 | lru_map "^0.3.3" 71 | tslib "^1.9.3" 72 | 73 | "@sentry/types@7.9.0": 74 | version "7.9.0" 75 | resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.9.0.tgz#8fa952865fda76f7c7c7fc6c84043979a22043ae" 76 | integrity sha512-VGnUgELVMpGJCYW1triO+5XSyaPjB2Zu6esUgbb8iJ5bi+OWtxklixXgwhdaTb0FDzmRL/T/pckmrIuBTLySHQ== 77 | 78 | "@sentry/utils@7.9.0": 79 | version "7.9.0" 80 | resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.9.0.tgz#497c41efe1b32974208ca68570e42c853b874f77" 81 | integrity sha512-4f9TZvAVopgG7Lp1TcPSekSX1Ashk68Et4T8Y+60EVX5se19i0hpytbHWWwrXSrb3w0KpGANk0byoZkdaTgkYA== 82 | dependencies: 83 | "@sentry/types" "7.9.0" 84 | tslib "^1.9.3" 85 | 86 | "@types/body-parser@*": 87 | version "1.19.2" 88 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" 89 | integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== 90 | dependencies: 91 | "@types/connect" "*" 92 | "@types/node" "*" 93 | 94 | "@types/connect@*": 95 | version "3.4.35" 96 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" 97 | integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== 98 | dependencies: 99 | "@types/node" "*" 100 | 101 | "@types/express-serve-static-core@^4.17.31": 102 | version "4.17.31" 103 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" 104 | integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== 105 | dependencies: 106 | "@types/node" "*" 107 | "@types/qs" "*" 108 | "@types/range-parser" "*" 109 | 110 | "@types/express@^4.17.15": 111 | version "4.17.15" 112 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" 113 | integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== 114 | dependencies: 115 | "@types/body-parser" "*" 116 | "@types/express-serve-static-core" "^4.17.31" 117 | "@types/qs" "*" 118 | "@types/serve-static" "*" 119 | 120 | "@types/luxon@^2.0.9": 121 | version "2.0.9" 122 | resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" 123 | integrity sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA== 124 | 125 | "@types/mime@*": 126 | version "3.0.1" 127 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" 128 | integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== 129 | 130 | "@types/node-fetch@^2.6.1": 131 | version "2.6.1" 132 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" 133 | integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== 134 | dependencies: 135 | "@types/node" "*" 136 | form-data "^3.0.0" 137 | 138 | "@types/node@*", "@types/node@^17.0.19": 139 | version "17.0.19" 140 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.19.tgz#726171367f404bfbe8512ba608a09ebad810c7e6" 141 | integrity sha512-PfeQhvcMR4cPFVuYfBN4ifG7p9c+Dlh3yUZR6k+5yQK7wX3gDgVxBly4/WkBRs9x4dmcy1TVl08SY67wwtEvmA== 142 | 143 | "@types/qs@*": 144 | version "6.9.7" 145 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" 146 | integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== 147 | 148 | "@types/range-parser@*": 149 | version "1.2.4" 150 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" 151 | integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== 152 | 153 | "@types/serve-static@*": 154 | version "1.15.0" 155 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" 156 | integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== 157 | dependencies: 158 | "@types/mime" "*" 159 | "@types/node" "*" 160 | 161 | accepts@~1.3.8: 162 | version "1.3.8" 163 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 164 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 165 | dependencies: 166 | mime-types "~2.1.34" 167 | negotiator "0.6.3" 168 | 169 | agent-base@6: 170 | version "6.0.2" 171 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" 172 | integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== 173 | dependencies: 174 | debug "4" 175 | 176 | amqplib@0.8.0: 177 | version "0.8.0" 178 | resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.8.0.tgz#088d10bc61cc5ac5a49ac72033c7ac66c23aeb61" 179 | integrity sha512-icU+a4kkq4Y1PS4NNi+YPDMwdlbFcZ1EZTQT2nigW3fvOb6AOgUQ9+Mk4ue0Zu5cBg/XpDzB40oH10ysrk2dmA== 180 | dependencies: 181 | bitsyntax "~0.1.0" 182 | bluebird "^3.7.2" 183 | buffer-more-ints "~1.0.0" 184 | readable-stream "1.x >=1.1.9" 185 | safe-buffer "~5.2.1" 186 | url-parse "~1.5.1" 187 | 188 | array-flatten@1.1.1: 189 | version "1.1.1" 190 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 191 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 192 | 193 | asynckit@^0.4.0: 194 | version "0.4.0" 195 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 196 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 197 | 198 | bitsyntax@~0.1.0: 199 | version "0.1.0" 200 | resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" 201 | integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== 202 | dependencies: 203 | buffer-more-ints "~1.0.0" 204 | debug "~2.6.9" 205 | safe-buffer "~5.1.2" 206 | 207 | bluebird@^3.7.2: 208 | version "3.7.2" 209 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" 210 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== 211 | 212 | body-parser@1.20.1: 213 | version "1.20.1" 214 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" 215 | integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== 216 | dependencies: 217 | bytes "3.1.2" 218 | content-type "~1.0.4" 219 | debug "2.6.9" 220 | depd "2.0.0" 221 | destroy "1.2.0" 222 | http-errors "2.0.0" 223 | iconv-lite "0.4.24" 224 | on-finished "2.4.1" 225 | qs "6.11.0" 226 | raw-body "2.5.1" 227 | type-is "~1.6.18" 228 | unpipe "1.0.0" 229 | 230 | buffer-more-ints@~1.0.0: 231 | version "1.0.0" 232 | resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" 233 | integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== 234 | 235 | bytes@3.1.2: 236 | version "3.1.2" 237 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 238 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 239 | 240 | call-bind@^1.0.0: 241 | version "1.0.2" 242 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 243 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 244 | dependencies: 245 | function-bind "^1.1.1" 246 | get-intrinsic "^1.0.2" 247 | 248 | cluster-key-slot@1.1.0: 249 | version "1.1.0" 250 | resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" 251 | integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== 252 | 253 | colorette@2.0.19: 254 | version "2.0.19" 255 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" 256 | integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== 257 | 258 | combined-stream@^1.0.8: 259 | version "1.0.8" 260 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 261 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 262 | dependencies: 263 | delayed-stream "~1.0.0" 264 | 265 | commander@^9.1.0: 266 | version "9.5.0" 267 | resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" 268 | integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== 269 | 270 | content-disposition@0.5.4: 271 | version "0.5.4" 272 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 273 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 274 | dependencies: 275 | safe-buffer "5.2.1" 276 | 277 | content-type@~1.0.4: 278 | version "1.0.4" 279 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 280 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 281 | 282 | cookie-signature@1.0.6: 283 | version "1.0.6" 284 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 285 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 286 | 287 | cookie@0.5.0: 288 | version "0.5.0" 289 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 290 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 291 | 292 | cookie@^0.4.1: 293 | version "0.4.2" 294 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" 295 | integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== 296 | 297 | core-util-is@~1.0.0: 298 | version "1.0.3" 299 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" 300 | integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== 301 | 302 | data-uri-to-buffer@^4.0.0: 303 | version "4.0.0" 304 | resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" 305 | integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== 306 | 307 | debug@2.6.9, debug@~2.6.9: 308 | version "2.6.9" 309 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 310 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 311 | dependencies: 312 | ms "2.0.0" 313 | 314 | debug@4, debug@4.3.4: 315 | version "4.3.4" 316 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 317 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 318 | dependencies: 319 | ms "2.1.2" 320 | 321 | delayed-stream@~1.0.0: 322 | version "1.0.0" 323 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 324 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 325 | 326 | denque@^2.0.1: 327 | version "2.0.1" 328 | resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" 329 | integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ== 330 | 331 | depd@2.0.0: 332 | version "2.0.0" 333 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 334 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 335 | 336 | destroy@1.2.0: 337 | version "1.2.0" 338 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 339 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 340 | 341 | dotenv@^16.0.0: 342 | version "16.0.0" 343 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" 344 | integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== 345 | 346 | ee-first@1.1.1: 347 | version "1.1.1" 348 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 349 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 350 | 351 | encodeurl@~1.0.2: 352 | version "1.0.2" 353 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 354 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 355 | 356 | escalade@^3.1.1: 357 | version "3.1.1" 358 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 359 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 360 | 361 | escape-html@~1.0.3: 362 | version "1.0.3" 363 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 364 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 365 | 366 | esm@^3.2.25: 367 | version "3.2.25" 368 | resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" 369 | integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== 370 | 371 | etag@~1.8.1: 372 | version "1.8.1" 373 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 374 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 375 | 376 | express@^4.18.2: 377 | version "4.18.2" 378 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" 379 | integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== 380 | dependencies: 381 | accepts "~1.3.8" 382 | array-flatten "1.1.1" 383 | body-parser "1.20.1" 384 | content-disposition "0.5.4" 385 | content-type "~1.0.4" 386 | cookie "0.5.0" 387 | cookie-signature "1.0.6" 388 | debug "2.6.9" 389 | depd "2.0.0" 390 | encodeurl "~1.0.2" 391 | escape-html "~1.0.3" 392 | etag "~1.8.1" 393 | finalhandler "1.2.0" 394 | fresh "0.5.2" 395 | http-errors "2.0.0" 396 | merge-descriptors "1.0.1" 397 | methods "~1.1.2" 398 | on-finished "2.4.1" 399 | parseurl "~1.3.3" 400 | path-to-regexp "0.1.7" 401 | proxy-addr "~2.0.7" 402 | qs "6.11.0" 403 | range-parser "~1.2.1" 404 | safe-buffer "5.2.1" 405 | send "0.18.0" 406 | serve-static "1.15.0" 407 | setprototypeof "1.2.0" 408 | statuses "2.0.1" 409 | type-is "~1.6.18" 410 | utils-merge "1.0.1" 411 | vary "~1.1.2" 412 | 413 | fetch-blob@^3.1.2, fetch-blob@^3.1.4: 414 | version "3.1.4" 415 | resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.4.tgz#e8c6567f80ad7fc22fd302e7dcb72bafde9c1717" 416 | integrity sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA== 417 | dependencies: 418 | node-domexception "^1.0.0" 419 | web-streams-polyfill "^3.0.3" 420 | 421 | finalhandler@1.2.0: 422 | version "1.2.0" 423 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 424 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 425 | dependencies: 426 | debug "2.6.9" 427 | encodeurl "~1.0.2" 428 | escape-html "~1.0.3" 429 | on-finished "2.4.1" 430 | parseurl "~1.3.3" 431 | statuses "2.0.1" 432 | unpipe "~1.0.0" 433 | 434 | form-data@^3.0.0: 435 | version "3.0.1" 436 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" 437 | integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== 438 | dependencies: 439 | asynckit "^0.4.0" 440 | combined-stream "^1.0.8" 441 | mime-types "^2.1.12" 442 | 443 | formdata-polyfill@^4.0.10: 444 | version "4.0.10" 445 | resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" 446 | integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== 447 | dependencies: 448 | fetch-blob "^3.1.2" 449 | 450 | forwarded@0.2.0: 451 | version "0.2.0" 452 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 453 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 454 | 455 | fresh@0.5.2: 456 | version "0.5.2" 457 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 458 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 459 | 460 | function-bind@^1.1.1: 461 | version "1.1.1" 462 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 463 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 464 | 465 | generate-function@^2.3.1: 466 | version "2.3.1" 467 | resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" 468 | integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== 469 | dependencies: 470 | is-property "^1.0.2" 471 | 472 | generic-pool@3.8.2: 473 | version "3.8.2" 474 | resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" 475 | integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== 476 | 477 | get-intrinsic@^1.0.2: 478 | version "1.1.3" 479 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" 480 | integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== 481 | dependencies: 482 | function-bind "^1.1.1" 483 | has "^1.0.3" 484 | has-symbols "^1.0.3" 485 | 486 | get-package-type@^0.1.0: 487 | version "0.1.0" 488 | resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" 489 | integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== 490 | 491 | getopts@2.3.0: 492 | version "2.3.0" 493 | resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" 494 | integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== 495 | 496 | has-symbols@^1.0.3: 497 | version "1.0.3" 498 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 499 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 500 | 501 | has@^1.0.3: 502 | version "1.0.3" 503 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 504 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 505 | dependencies: 506 | function-bind "^1.1.1" 507 | 508 | http-errors@2.0.0: 509 | version "2.0.0" 510 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 511 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 512 | dependencies: 513 | depd "2.0.0" 514 | inherits "2.0.4" 515 | setprototypeof "1.2.0" 516 | statuses "2.0.1" 517 | toidentifier "1.0.1" 518 | 519 | https-proxy-agent@^5.0.0: 520 | version "5.0.1" 521 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" 522 | integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== 523 | dependencies: 524 | agent-base "6" 525 | debug "4" 526 | 527 | iconv-lite@0.4.24: 528 | version "0.4.24" 529 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 530 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 531 | dependencies: 532 | safer-buffer ">= 2.1.2 < 3" 533 | 534 | iconv-lite@^0.6.3: 535 | version "0.6.3" 536 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" 537 | integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== 538 | dependencies: 539 | safer-buffer ">= 2.1.2 < 3.0.0" 540 | 541 | inherits@2.0.4, inherits@~2.0.1: 542 | version "2.0.4" 543 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 544 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 545 | 546 | interpret@^2.2.0: 547 | version "2.2.0" 548 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" 549 | integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== 550 | 551 | ipaddr.js@1.9.1: 552 | version "1.9.1" 553 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 554 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 555 | 556 | is-core-module@^2.8.1: 557 | version "2.8.1" 558 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" 559 | integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== 560 | dependencies: 561 | has "^1.0.3" 562 | 563 | is-property@^1.0.2: 564 | version "1.0.2" 565 | resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" 566 | integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= 567 | 568 | isarray@0.0.1: 569 | version "0.0.1" 570 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 571 | integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= 572 | 573 | knex@^2.4.0: 574 | version "2.4.0" 575 | resolved "https://registry.yarnpkg.com/knex/-/knex-2.4.0.tgz#7d33cc36f320cdac98741010544b4c6a98b8b19e" 576 | integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA== 577 | dependencies: 578 | colorette "2.0.19" 579 | commander "^9.1.0" 580 | debug "4.3.4" 581 | escalade "^3.1.1" 582 | esm "^3.2.25" 583 | get-package-type "^0.1.0" 584 | getopts "2.3.0" 585 | interpret "^2.2.0" 586 | lodash "^4.17.21" 587 | pg-connection-string "2.5.0" 588 | rechoir "^0.8.0" 589 | resolve-from "^5.0.0" 590 | tarn "^3.0.2" 591 | tildify "2.0.0" 592 | 593 | lodash@^4.17.21: 594 | version "4.17.21" 595 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 596 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 597 | 598 | long@^4.0.0: 599 | version "4.0.0" 600 | resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 601 | integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== 602 | 603 | lru-cache@^4.1.3: 604 | version "4.1.5" 605 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 606 | integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 607 | dependencies: 608 | pseudomap "^1.0.2" 609 | yallist "^2.1.2" 610 | 611 | lru-cache@^6.0.0: 612 | version "6.0.0" 613 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 614 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 615 | dependencies: 616 | yallist "^4.0.0" 617 | 618 | lru_map@^0.3.3: 619 | version "0.3.3" 620 | resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" 621 | integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== 622 | 623 | luxon@^2.3.1: 624 | version "2.3.1" 625 | resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.1.tgz#f276b1b53fd9a740a60e666a541a7f6dbed4155a" 626 | integrity sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA== 627 | 628 | media-typer@0.3.0: 629 | version "0.3.0" 630 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 631 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 632 | 633 | merge-descriptors@1.0.1: 634 | version "1.0.1" 635 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 636 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 637 | 638 | methods@~1.1.2: 639 | version "1.1.2" 640 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 641 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 642 | 643 | mime-db@1.51.0: 644 | version "1.51.0" 645 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" 646 | integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== 647 | 648 | mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: 649 | version "2.1.34" 650 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" 651 | integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== 652 | dependencies: 653 | mime-db "1.51.0" 654 | 655 | mime@1.6.0: 656 | version "1.6.0" 657 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 658 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 659 | 660 | ms@2.0.0: 661 | version "2.0.0" 662 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 663 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 664 | 665 | ms@2.1.2: 666 | version "2.1.2" 667 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 668 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 669 | 670 | ms@2.1.3: 671 | version "2.1.3" 672 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 673 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 674 | 675 | mysql2@^2.3.3: 676 | version "2.3.3" 677 | resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0" 678 | integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA== 679 | dependencies: 680 | denque "^2.0.1" 681 | generate-function "^2.3.1" 682 | iconv-lite "^0.6.3" 683 | long "^4.0.0" 684 | lru-cache "^6.0.0" 685 | named-placeholders "^1.1.2" 686 | seq-queue "^0.0.5" 687 | sqlstring "^2.3.2" 688 | 689 | named-placeholders@^1.1.2: 690 | version "1.1.2" 691 | resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.2.tgz#ceb1fbff50b6b33492b5cf214ccf5e39cef3d0e8" 692 | integrity sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA== 693 | dependencies: 694 | lru-cache "^4.1.3" 695 | 696 | negotiator@0.6.3: 697 | version "0.6.3" 698 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 699 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 700 | 701 | node-domexception@^1.0.0: 702 | version "1.0.0" 703 | resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" 704 | integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== 705 | 706 | node-fetch@^3.1.0, node-fetch@^3.2.10: 707 | version "3.2.10" 708 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" 709 | integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== 710 | dependencies: 711 | data-uri-to-buffer "^4.0.0" 712 | fetch-blob "^3.1.4" 713 | formdata-polyfill "^4.0.10" 714 | 715 | object-inspect@^1.9.0: 716 | version "1.12.2" 717 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" 718 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== 719 | 720 | on-finished@2.4.1: 721 | version "2.4.1" 722 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 723 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 724 | dependencies: 725 | ee-first "1.1.1" 726 | 727 | parseurl@~1.3.3: 728 | version "1.3.3" 729 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 730 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 731 | 732 | path-parse@^1.0.7: 733 | version "1.0.7" 734 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 735 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 736 | 737 | path-to-regexp@0.1.7: 738 | version "0.1.7" 739 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 740 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 741 | 742 | pg-connection-string@2.5.0: 743 | version "2.5.0" 744 | resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" 745 | integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== 746 | 747 | proxy-addr@~2.0.7: 748 | version "2.0.7" 749 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 750 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 751 | dependencies: 752 | forwarded "0.2.0" 753 | ipaddr.js "1.9.1" 754 | 755 | pseudomap@^1.0.2: 756 | version "1.0.2" 757 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 758 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= 759 | 760 | qs@6.11.0: 761 | version "6.11.0" 762 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 763 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 764 | dependencies: 765 | side-channel "^1.0.4" 766 | 767 | querystringify@^2.1.1: 768 | version "2.2.0" 769 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" 770 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 771 | 772 | rabbit-queue@^5.4.1: 773 | version "5.4.1" 774 | resolved "https://registry.yarnpkg.com/rabbit-queue/-/rabbit-queue-5.4.1.tgz#47a0ee3faa8cb98059c186a552b1d87e84b9892d" 775 | integrity sha512-S7SCaeBhruwPbUxj8BvEp/XR+s23IsuDOCpeqxz+akH/mqI56FNAzjbKaQ3J1GxQhGR77tz9dWO5Jv93egVxiA== 776 | dependencies: 777 | amqplib "0.8.0" 778 | race-until "~2.3.1" 779 | uuid "~8.3.2" 780 | 781 | race-until@~2.3.1: 782 | version "2.3.1" 783 | resolved "https://registry.yarnpkg.com/race-until/-/race-until-2.3.1.tgz#2ac270684ab8ea1d6e17842b2b4be6cce66a76ca" 784 | integrity sha512-NUpJpbEaZ3FL5UprkqzvvsY5HNEfJ8s1ba+MtQGdGBskm5u1K4tX546oi7BOjkw3LTtykOPQ///a7nKGEUd3uQ== 785 | 786 | range-parser@~1.2.1: 787 | version "1.2.1" 788 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 789 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 790 | 791 | raw-body@2.5.1: 792 | version "2.5.1" 793 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 794 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 795 | dependencies: 796 | bytes "3.1.2" 797 | http-errors "2.0.0" 798 | iconv-lite "0.4.24" 799 | unpipe "1.0.0" 800 | 801 | "readable-stream@1.x >=1.1.9": 802 | version "1.1.14" 803 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 804 | integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= 805 | dependencies: 806 | core-util-is "~1.0.0" 807 | inherits "~2.0.1" 808 | isarray "0.0.1" 809 | string_decoder "~0.10.x" 810 | 811 | rechoir@^0.8.0: 812 | version "0.8.0" 813 | resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" 814 | integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== 815 | dependencies: 816 | resolve "^1.20.0" 817 | 818 | redis-errors@^1.0.0: 819 | version "1.2.0" 820 | resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" 821 | integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= 822 | 823 | redis-parser@3.0.0: 824 | version "3.0.0" 825 | resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" 826 | integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= 827 | dependencies: 828 | redis-errors "^1.0.0" 829 | 830 | redis@^4.0.4: 831 | version "4.0.4" 832 | resolved "https://registry.yarnpkg.com/redis/-/redis-4.0.4.tgz#b567f82f59086df38433982f7f424b48e924ec7a" 833 | integrity sha512-KaM1OAj/nGrSeybmmOWSMY0LXTGT6FVWgUZZrd2MYzXKJ+VGtqVaciGQeNMfZiQX+kDM8Ke4uttb54m2rm6V0A== 834 | dependencies: 835 | "@node-redis/bloom" "1.0.1" 836 | "@node-redis/client" "1.0.4" 837 | "@node-redis/graph" "1.0.0" 838 | "@node-redis/json" "1.0.2" 839 | "@node-redis/search" "1.0.3" 840 | "@node-redis/time-series" "1.0.2" 841 | 842 | requires-port@^1.0.0: 843 | version "1.0.0" 844 | resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 845 | integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 846 | 847 | resolve-from@^5.0.0: 848 | version "5.0.0" 849 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" 850 | integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== 851 | 852 | resolve@^1.20.0: 853 | version "1.22.0" 854 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" 855 | integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== 856 | dependencies: 857 | is-core-module "^2.8.1" 858 | path-parse "^1.0.7" 859 | supports-preserve-symlinks-flag "^1.0.0" 860 | 861 | safe-buffer@5.2.1, safe-buffer@~5.2.1: 862 | version "5.2.1" 863 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 864 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 865 | 866 | safe-buffer@~5.1.2: 867 | version "5.1.2" 868 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 869 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 870 | 871 | "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": 872 | version "2.1.2" 873 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 874 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 875 | 876 | send@0.18.0: 877 | version "0.18.0" 878 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 879 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 880 | dependencies: 881 | debug "2.6.9" 882 | depd "2.0.0" 883 | destroy "1.2.0" 884 | encodeurl "~1.0.2" 885 | escape-html "~1.0.3" 886 | etag "~1.8.1" 887 | fresh "0.5.2" 888 | http-errors "2.0.0" 889 | mime "1.6.0" 890 | ms "2.1.3" 891 | on-finished "2.4.1" 892 | range-parser "~1.2.1" 893 | statuses "2.0.1" 894 | 895 | seq-queue@^0.0.5: 896 | version "0.0.5" 897 | resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" 898 | integrity sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4= 899 | 900 | serve-static@1.15.0: 901 | version "1.15.0" 902 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 903 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 904 | dependencies: 905 | encodeurl "~1.0.2" 906 | escape-html "~1.0.3" 907 | parseurl "~1.3.3" 908 | send "0.18.0" 909 | 910 | setprototypeof@1.2.0: 911 | version "1.2.0" 912 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 913 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 914 | 915 | shoutrrrd-js@^1.0.3: 916 | version "1.0.3" 917 | resolved "https://registry.yarnpkg.com/shoutrrrd-js/-/shoutrrrd-js-1.0.3.tgz#aae164b88679e2d8488dedaf69ac6364277adb33" 918 | integrity sha512-bc13acMYEsFscjZ8jDklMq4sCbIA1hY3MWZ0yUXks+2C0oJ0NNqG7lgqplMoXy/tiLe+1ppwmDA6B5lq/YEp/Q== 919 | dependencies: 920 | node-fetch "^3.1.0" 921 | 922 | side-channel@^1.0.4: 923 | version "1.0.4" 924 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 925 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 926 | dependencies: 927 | call-bind "^1.0.0" 928 | get-intrinsic "^1.0.2" 929 | object-inspect "^1.9.0" 930 | 931 | sqlstring@^2.3.2: 932 | version "2.3.2" 933 | resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" 934 | integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg== 935 | 936 | statuses@2.0.1: 937 | version "2.0.1" 938 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 939 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 940 | 941 | string_decoder@~0.10.x: 942 | version "0.10.31" 943 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 944 | integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= 945 | 946 | supports-preserve-symlinks-flag@^1.0.0: 947 | version "1.0.0" 948 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 949 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 950 | 951 | tarn@^3.0.2: 952 | version "3.0.2" 953 | resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" 954 | integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== 955 | 956 | tildify@2.0.0: 957 | version "2.0.0" 958 | resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" 959 | integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== 960 | 961 | toidentifier@1.0.1: 962 | version "1.0.1" 963 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 964 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 965 | 966 | tslib@^1.9.3: 967 | version "1.14.1" 968 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 969 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 970 | 971 | type-is@~1.6.18: 972 | version "1.6.18" 973 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 974 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 975 | dependencies: 976 | media-typer "0.3.0" 977 | mime-types "~2.1.24" 978 | 979 | typescript@^4.5.5: 980 | version "4.5.5" 981 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" 982 | integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== 983 | 984 | unpipe@1.0.0, unpipe@~1.0.0: 985 | version "1.0.0" 986 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 987 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 988 | 989 | url-parse@~1.5.1: 990 | version "1.5.10" 991 | resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" 992 | integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== 993 | dependencies: 994 | querystringify "^2.1.1" 995 | requires-port "^1.0.0" 996 | 997 | utils-merge@1.0.1: 998 | version "1.0.1" 999 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1000 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 1001 | 1002 | uuid@~8.3.2: 1003 | version "8.3.2" 1004 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" 1005 | integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== 1006 | 1007 | vary@~1.1.2: 1008 | version "1.1.2" 1009 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1010 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1011 | 1012 | web-streams-polyfill@^3.0.3: 1013 | version "3.2.0" 1014 | resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" 1015 | integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== 1016 | 1017 | yallist@4.0.0, yallist@^4.0.0: 1018 | version "4.0.0" 1019 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 1020 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1021 | 1022 | yallist@^2.1.2: 1023 | version "2.1.2" 1024 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 1025 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 1026 | --------------------------------------------------------------------------------