diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd7bca7..9332a98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,4 +23,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - name: run tests - run: pnpm test + run: pnpm test:coverage + - name: "Report Coverage" + uses: davelosert/vitest-coverage-report-action@v2 diff --git a/Dockerfile b/Dockerfile index bac0ba7..d964c25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# this is the file that railway will run FROM node:24-slim AS base ENV PNPM_HOME="/pnpm" # ENV PATH="$PNPM_HOME:$PATH" diff --git a/README.md b/README.md index 76b3892..2b40bbf 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ Now install the API's dependencies by 'cd'-ing into the root of the repository a pnpm install ``` -Start your local database with `pnpm db:start` and then start the server with `pnpm dev` and it should work, assuming you have the correct env variables. (To see the contents of the database, I recommend using DBeaver. You can also run `pnpm db:studio` to start up drizzle studio) +Start your local database with `pnpm db:start`, `pnpm db:push` (if this is your first time) and then start the server with `pnpm dev` and it should work, assuming you have the correct env variables. (To see the contents of the database, I recommend using DBeaver. You can also run `pnpm db:studio` to start up drizzle studio) ## Database schema changes (important!) When you make changes to the database schema, be sure to run `pnpm db:push` to keep your local db in sync. -Before merging your PR, be sure to run `pnpm db:generate` to generate a migration file, which will then be automatically applied to the staging and production databases when deployed. +Before merging your PR, be sure to run `pnpm db:generate` to generate a migration file, which will then be automatically applied to the staging and production databases when deployed. (You should do this before running tests as well!) To test if the migration files work, you can run `pnpm run-prod`, which will spin up a production version of the server and a postgres database mounted on a new volume. The server is created using the same Dockerfile used in our Railway deployments, so if it works locally, it (probably) works in production as well. diff --git a/drizzle/0001_broad_lord_tyger.sql b/drizzle/0001_broad_lord_tyger.sql new file mode 100644 index 0000000..e0f8c4d --- /dev/null +++ b/drizzle/0001_broad_lord_tyger.sql @@ -0,0 +1,57 @@ +CREATE TYPE "public"."specialType" AS ENUM('special', 'soup');--> statement-breakpoint +CREATE TABLE "concept_id_to_internal_id" ( + "internal_id" text NOT NULL, + "external_id" text PRIMARY KEY NOT NULL, + CONSTRAINT "concept_id_to_internal_id_external_id_unique" UNIQUE("external_id") +); +--> statement-breakpoint +CREATE TABLE "location_data" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "short_description" text, + "description" text NOT NULL, + "url" text NOT NULL, + "menu" text, + "location" text NOT NULL, + "coordinate_lat" numeric, + "coordinate_lng" numeric, + "acceptsOnlineOrders" boolean NOT NULL +); +--> statement-breakpoint +CREATE TABLE "specials" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "specials_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "location_id" text NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + "date" date NOT NULL, + "type" "specialType" NOT NULL +); +--> statement-breakpoint +CREATE TABLE "time_overwrites_table" ( + "location_id" text NOT NULL, + "date" date NOT NULL, + "time_string" text NOT NULL, + CONSTRAINT "time_overwrites_table_location_id_date_pk" PRIMARY KEY("location_id","date") +); +--> statement-breakpoint +CREATE TABLE "location_times" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "location_times_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "location_id" text NOT NULL, + "date" date NOT NULL, + "start_time" integer NOT NULL, + "end_time" integer NOT NULL +); +--> statement-breakpoint +ALTER TABLE "overwrites_table" RENAME COLUMN "concept_id" TO "location_id";--> statement-breakpoint + +-- Manual +ALTER TABLE "overwrites_table" ALTER COLUMN "location_id" TYPE TEXT, ALTER COLUMN "location_id" SET NOT NULL; --> statement-breakpoint +-- Manual + +ALTER TABLE "concept_id_to_internal_id" ADD CONSTRAINT "concept_id_to_internal_id_internal_id_location_data_id_fk" FOREIGN KEY ("internal_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "specials" ADD CONSTRAINT "specials_location_id_location_data_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "time_overwrites_table" ADD CONSTRAINT "time_overwrites_table_location_id_location_data_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "location_times" ADD CONSTRAINT "location_times_location_id_location_data_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "date_lookup" ON "location_times" USING btree ("location_id","date");--> statement-breakpoint +ALTER TABLE "overwrites_table" ADD CONSTRAINT "overwrites_table_location_id_location_data_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "overwrites_table" DROP COLUMN "times"; \ No newline at end of file diff --git a/drizzle/0002_fair_meteorite.sql b/drizzle/0002_fair_meteorite.sql new file mode 100644 index 0000000..cb7d028 --- /dev/null +++ b/drizzle/0002_fair_meteorite.sql @@ -0,0 +1,3 @@ +ALTER TABLE "location_data" RENAME COLUMN "acceptsOnlineOrders" TO "accepts_online_orders";--> statement-breakpoint +ALTER TABLE "overwrites_table" RENAME COLUMN "coordinateLat" TO "coordinate_lat";--> statement-breakpoint +ALTER TABLE "overwrites_table" RENAME COLUMN "coordinateLng" TO "coordinate_lng"; \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..9cbe72e --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,495 @@ +{ + "id": "6cb70ab4-5a52-45dc-b62f-616000bbf5ec", + "prevId": "51fd9b98-60b4-4660-92b5-fa6adeb2b033", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.concept_id_to_internal_id": { + "name": "concept_id_to_internal_id", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "concept_id_to_internal_id_internal_id_location_data_id_fk": { + "name": "concept_id_to_internal_id_internal_id_location_data_id_fk", + "tableFrom": "concept_id_to_internal_id", + "tableTo": "location_data", + "columnsFrom": [ + "internal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "concept_id_to_internal_id_external_id_unique": { + "name": "concept_id_to_internal_id_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.emails": { + "name": "emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "emails_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location_data": { + "name": "location_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short_description": { + "name": "short_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "menu": { + "name": "menu", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "coordinate_lat": { + "name": "coordinate_lat", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "coordinate_lng": { + "name": "coordinate_lng", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "acceptsOnlineOrders": { + "name": "acceptsOnlineOrders", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.overwrites_table": { + "name": "overwrites_table", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short_description": { + "name": "short_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "menu": { + "name": "menu", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinateLat": { + "name": "coordinateLat", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "coordinateLng": { + "name": "coordinateLng", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "accepts_online_orders": { + "name": "accepts_online_orders", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "overwrites_table_location_id_location_data_id_fk": { + "name": "overwrites_table_location_id_location_data_id_fk", + "tableFrom": "overwrites_table", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.specials": { + "name": "specials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "specials_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "specialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "specials_location_id_location_data_id_fk": { + "name": "specials_location_id_location_data_id_fk", + "tableFrom": "specials", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.time_overwrites_table": { + "name": "time_overwrites_table", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "time_string": { + "name": "time_string", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "time_overwrites_table_location_id_location_data_id_fk": { + "name": "time_overwrites_table_location_id_location_data_id_fk", + "tableFrom": "time_overwrites_table", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "time_overwrites_table_location_id_date_pk": { + "name": "time_overwrites_table_location_id_date_pk", + "columns": [ + "location_id", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location_times": { + "name": "location_times", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "location_times_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "date_lookup": { + "name": "date_lookup", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "location_times_location_id_location_data_id_fk": { + "name": "location_times_location_id_location_data_id_fk", + "tableFrom": "location_times", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.specialType": { + "name": "specialType", + "schema": "public", + "values": [ + "special", + "soup" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..ea2ad8f --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,495 @@ +{ + "id": "0cafcb54-9cfd-41e1-b46c-748f17a03c53", + "prevId": "6cb70ab4-5a52-45dc-b62f-616000bbf5ec", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.concept_id_to_internal_id": { + "name": "concept_id_to_internal_id", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "concept_id_to_internal_id_internal_id_location_data_id_fk": { + "name": "concept_id_to_internal_id_internal_id_location_data_id_fk", + "tableFrom": "concept_id_to_internal_id", + "tableTo": "location_data", + "columnsFrom": [ + "internal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "concept_id_to_internal_id_external_id_unique": { + "name": "concept_id_to_internal_id_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.emails": { + "name": "emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "emails_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location_data": { + "name": "location_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short_description": { + "name": "short_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "menu": { + "name": "menu", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "coordinate_lat": { + "name": "coordinate_lat", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "coordinate_lng": { + "name": "coordinate_lng", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "accepts_online_orders": { + "name": "accepts_online_orders", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.overwrites_table": { + "name": "overwrites_table", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short_description": { + "name": "short_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "menu": { + "name": "menu", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinate_lat": { + "name": "coordinate_lat", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "coordinate_lng": { + "name": "coordinate_lng", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "accepts_online_orders": { + "name": "accepts_online_orders", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "overwrites_table_location_id_location_data_id_fk": { + "name": "overwrites_table_location_id_location_data_id_fk", + "tableFrom": "overwrites_table", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.specials": { + "name": "specials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "specials_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "specialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "specials_location_id_location_data_id_fk": { + "name": "specials_location_id_location_data_id_fk", + "tableFrom": "specials", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.time_overwrites_table": { + "name": "time_overwrites_table", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "time_string": { + "name": "time_string", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "time_overwrites_table_location_id_location_data_id_fk": { + "name": "time_overwrites_table_location_id_location_data_id_fk", + "tableFrom": "time_overwrites_table", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "time_overwrites_table_location_id_date_pk": { + "name": "time_overwrites_table_location_id_date_pk", + "columns": [ + "location_id", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location_times": { + "name": "location_times", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "location_times_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "date_lookup": { + "name": "date_lookup", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "location_times_location_id_location_data_id_fk": { + "name": "location_times_location_id_location_data_id_fk", + "tableFrom": "location_times", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.specialType": { + "name": "specialType", + "schema": "public", + "values": [ + "special", + "soup" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 47b6d21..b3c252f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1757973116166, "tag": "0000_wise_firestar", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1762796324448, + "tag": "0001_broad_lord_tyger", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1763850203721, + "tag": "0002_fair_meteorite", + "breakpoints": true } ] } \ No newline at end of file diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 0abdfd9..0000000 --- a/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ -export default { - testEnvironment: "node", - transform: { - "^.+.tsx?$": ["ts-jest", { diagnostics: { warnOnly: true } }], - }, - moduleDirectories: ['node_modules', 'src'] -}; \ No newline at end of file diff --git a/package.json b/package.json index 9f47e02..61c64dd 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,17 @@ "dev": "dotenv -- tsx watch src/server.ts", "build": "rollup -c", "start": "NODE_ENV=production node dist/server.js", - "test": "dotenv -e .env.test -- jest --coverage", + "test": "dotenv -e .env.test -- vitest", + "test:coverage": "dotenv -e .env.test -- vitest --coverage", + "__comment": "add DEBUG=testcontainers* to the test command to get testcontainers debug output", + "typecheck": "tsc", "run-prod": "VOL_SRC=postgres_data docker-compose up --build", "db:start": "VOL_SRC=postgres_dev_data docker-compose up -d postgres --build", "db:push": "drizzle-kit push", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "db:check-migrations": "drizzle-kit check" }, "repository": { "type": "git", @@ -27,14 +31,17 @@ "dependencies": { "@elysiajs/cors": "1.3.3", "@elysiajs/node": "1.3.0", + "@elysiajs/openapi": "^1.4.11", "@types/express": "^5.0.3", "axios": "^1.10.0", "cheerio": "1.0.0-rc.12", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.4", - "elysia": "1.3.5", + "elysia": "1.4.15", "express": "^5.1.0", + "luxon": "^3.7.2", "pg": "^8.16.3", + "vite-tsconfig-paths": "^5.1.4", "zod": "^3.25.67" }, "devDependencies": { @@ -42,18 +49,19 @@ "@babel/preset-env": "^7.25.4", "@babel/preset-typescript": "^7.24.7", "@rollup/plugin-typescript": "^12.1.4", - "@types/jest": "^29.5.14", + "@testcontainers/postgresql": "^11.8.0", + "@types/luxon": "^3.7.1", "@types/node": "^24.0.14", "@types/pg": "^8.15.5", - "babel-jest": "^29.7.0", + "@vitest/coverage-v8": "4.0.8", "dotenv": "^17.2.0", "dotenv-cli": "^8.0.0", - "jest": "^29.7.0", "npm-check-updates": "^18.0.1", "rollup": "^4.45.1", - "ts-jest": "^29.2.5", + "testcontainers": "^11.8.0", "tslib": "^2.8.1", - "tsx": "^4.20.3" + "tsx": "^4.20.3", + "vitest": "^4.0.8" }, "peerDependencies": { "typescript": "^5.8.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65d8336..75465ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,13 @@ importers: dependencies: '@elysiajs/cors': specifier: 1.3.3 - version: 1.3.3(elysia@1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2)) + version: 1.3.3(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2)) '@elysiajs/node': specifier: 1.3.0 - version: 1.3.0(elysia@1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2))(hono@4.9.8) + version: 1.3.0(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2))(hono@4.9.8) + '@elysiajs/openapi': + specifier: ^1.4.11 + version: 1.4.11(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2)) '@types/express': specifier: ^5.0.3 version: 5.0.3 @@ -30,17 +33,23 @@ importers: specifier: ^0.44.4 version: 0.44.5(@types/pg@8.15.5)(bun-types@1.2.22(@types/react@19.1.13))(pg@8.16.3) elysia: - specifier: 1.3.5 - version: 1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2) + specifier: 1.4.15 + version: 1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) express: specifier: ^5.1.0 version: 5.1.0 + luxon: + specifier: ^3.7.2 + version: 3.7.2 pg: specifier: ^8.16.3 version: 8.16.3 typescript: specifier: ^5.8.3 version: 5.9.2 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) zod: specifier: ^3.25.67 version: 3.25.76 @@ -57,42 +66,45 @@ importers: '@rollup/plugin-typescript': specifier: ^12.1.4 version: 12.1.4(rollup@4.52.0)(tslib@2.8.1)(typescript@5.9.2) - '@types/jest': - specifier: ^29.5.14 - version: 29.5.14 + '@testcontainers/postgresql': + specifier: ^11.8.0 + version: 11.8.0 + '@types/luxon': + specifier: ^3.7.1 + version: 3.7.1 '@types/node': specifier: ^24.0.14 version: 24.5.2 '@types/pg': specifier: ^8.15.5 version: 8.15.5 - babel-jest: - specifier: ^29.7.0 - version: 29.7.0(@babel/core@7.28.4) + '@vitest/coverage-v8': + specifier: 4.0.8 + version: 4.0.8(vitest@4.0.8(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) dotenv: specifier: ^17.2.0 version: 17.2.2 dotenv-cli: specifier: ^8.0.0 version: 8.0.0 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@24.5.2) npm-check-updates: specifier: ^18.0.1 version: 18.3.0 rollup: specifier: ^4.45.1 version: 4.52.0 - ts-jest: - specifier: ^29.2.5 - version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@24.5.2))(typescript@5.9.2) + testcontainers: + specifier: ^11.8.0 + version: 11.8.0 tslib: specifier: ^2.8.1 version: 2.8.1 tsx: specifier: ^4.20.3 version: 4.20.5 + vitest: + specifier: ^4.0.8 + version: 4.0.8(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) packages: @@ -187,6 +199,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -204,6 +220,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -240,27 +261,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} @@ -273,64 +273,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} @@ -684,8 +632,16 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} @@ -703,6 +659,11 @@ packages: peerDependencies: elysia: '>= 1.3.3' + '@elysiajs/openapi@1.4.11': + resolution: {integrity: sha512-d75bMxYJpN6qSDi/z9L1S7SLk1S/8Px+cTb3W2lrYzU8uQ5E0kXdy1oOMJEfTyVsz3OA19NP9KNxE7ztSbLBLg==} + peerDependencies: + elysia: '>= 1.4.0' + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -999,101 +960,82 @@ packages: cpu: [x64] os: [win32] + '@grpc/grpc-js@1.14.1': + resolution: {integrity: sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@hono/node-server@1.19.3': resolution: {integrity: sha512-Fjyxfux0rMPXMSob79OmddfpK5ArJa2xLkLCV+zamHkbeXQtSNKOi0keiBKyHZ/hCRKjigjmKGp4AJnDFq8PUw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} '@rollup/plugin-typescript@12.1.4': resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} @@ -1227,17 +1169,14 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@testcontainers/postgresql@11.8.0': + resolution: {integrity: sha512-JTMBEoLbi1eYvbsQTqbQlPVED07EB4xhB1tAkmvmirmLsjT0IH6YPKZdZD5y91KKR/FDvSbhPsYmDtByGyGqwg==} '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} @@ -1246,24 +1185,24 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.45': + resolution: {integrity: sha512-iYpZF+xr5QLpIICejLdUF2r5gh8IXY1Gw3WLmt41dUbS3Vn/3hVgL+6lJBVbmrhYBWfbWPPstdr6+A0s95DTWA==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1273,27 +1212,21 @@ packages: '@types/express@5.0.3': resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@24.10.0': + resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + '@types/node@24.5.2': resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} @@ -1315,41 +1248,100 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + + '@vitest/coverage-v8@4.0.8': + resolution: {integrity: sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A==} + peerDependencies: + '@vitest/browser': 4.0.8 + vitest: 4.0.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.0.8': + resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} + + '@vitest/mocker@4.0.8': + resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.8': + resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} + + '@vitest/runner@4.0.8': + resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==} + + '@vitest/snapshot@4.0.8': + resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} + + '@vitest/spy@4.0.8': + resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@vitest/utils@4.0.8': + resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.8: + resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1357,19 +1349,13 @@ packages: axios@1.12.2: resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} @@ -1386,24 +1372,60 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + bare-fs@4.5.0: + resolution: {integrity: sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==} + engines: {bare: '>=1.16.0'} peerDependencies: - '@babel/core': ^7.0.0 + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} baseline-browser-mapping@2.8.6: resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1411,33 +1433,40 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} browserslist@4.26.2: resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + buildcheck@0.0.6: + resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + engines: {node: '>=10.0.0'} + bun-types@1.2.22: resolution: {integrity: sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==} peerDependencies: '@types/react': ^19 + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1450,28 +1479,12 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - caniuse-lite@1.0.30001743: resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + chai@6.2.0: + resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} + engines: {node: '>=18'} cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -1480,24 +1493,13 @@ packages: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} engines: {node: '>= 6'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1509,8 +1511,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} @@ -1538,11 +1541,22 @@ packages: core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} hasBin: true + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1566,18 +1580,6 @@ packages: supports-color: optional: true - dedent@1.7.0: - resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1586,13 +1588,17 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} + docker-compose@1.3.0: + resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} + engines: {node: '>= 6.0.0'} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + docker-modem@5.0.6: + resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} + engines: {node: '>= 8.0'} + + dockerode@4.0.9: + resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==} + engines: {node: '>= 8.0'} dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -1723,30 +1729,43 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} electron-to-chromium@1.5.222: resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==} - elysia@1.3.5: - resolution: {integrity: sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw==} + elysia@1.4.15: + resolution: {integrity: sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg==} peerDependencies: + '@sinclair/typebox': '>= 0.34.0 < 1' + '@types/bun': '>= 1.2.0' exact-mirror: '>= 0.0.9' file-type: '>= 20.0.0' + openapi-types: '>= 12.0.0' typescript: '>= 5.0.0' - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + peerDependenciesMeta: + '@types/bun': + optional: true + typescript: + optional: true emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1755,9 +1774,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1766,6 +1782,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1796,18 +1815,12 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1816,6 +1829,17 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + exact-mirror@0.2.2: resolution: {integrity: sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA==} peerDependencies: @@ -1824,17 +1848,9 @@ packages: '@sinclair/typebox': optional: true - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} @@ -1843,11 +1859,17 @@ packages: fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -1856,18 +1878,10 @@ packages: resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} engines: {node: '>=20'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -1877,6 +1891,10 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} @@ -1889,8 +1907,8 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1912,24 +1930,23 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} @@ -1938,11 +1955,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1973,10 +1985,6 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1988,19 +1996,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2008,9 +2003,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -2019,14 +2011,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -2034,6 +2018,9 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2041,161 +2028,26 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} @@ -2207,48 +2059,47 @@ packages: engines: {node: '>=6'} hasBin: true - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2257,17 +2108,13 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + memoirist@0.4.0: + resolution: {integrity: sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2284,32 +2131,44 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nan@2.23.1: + resolution: {integrity: sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -2322,10 +2181,6 @@ packages: engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'} hasBin: true - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2340,32 +2195,11 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -2377,14 +2211,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2392,9 +2218,16 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -2432,21 +2265,13 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} @@ -2464,13 +2289,23 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@2.3.0: + resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} + engines: {node: '>=14'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} @@ -2479,8 +2314,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} @@ -2494,8 +2329,19 @@ packages: resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} engines: {node: '>= 0.10'} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} regenerate-unicode-properties@10.2.2: resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} @@ -2519,26 +2365,18 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} hasBin: true + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rollup@4.52.0: resolution: {integrity: sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2548,6 +2386,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2558,8 +2399,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true @@ -2598,18 +2439,19 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -2618,16 +2460,22 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} @@ -2637,30 +2485,34 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -2669,24 +2521,46 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + testcontainers@11.8.0: + resolution: {integrity: sha512-kY2DfuUB1NSvmpG7wCpi/aTaIJaHcX53WSAlWHsj0La7E7fPnVFOpooheczE3fH9T+OgD5OB5IeBpFitIqqu6w==} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -2696,31 +2570,14 @@ packages: resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} - ts-jest@29.4.4: - resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} hasBin: true peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' + typescript: ^5.0.0 peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: + typescript: optional: true tslib@2.8.1: @@ -2731,17 +2588,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} @@ -2752,18 +2600,23 @@ packages: engines: {node: '>=14.17'} hasBin: true - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.12.0: resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@7.16.0: + resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -2790,36 +2643,120 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.2.2: + resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.8: + resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.8 + '@vitest/browser-preview': 4.0.8 + '@vitest/browser-webdriverio': 4.0.8 + '@vitest/ui': 4.0.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2831,6 +2768,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2839,9 +2781,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2987,6 +2929,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.3': @@ -3006,6 +2950,10 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -3045,189 +2993,119 @@ snapshots: dependencies: '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/template': 7.27.2 - - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) @@ -3602,23 +3480,34 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@bcoe/v8-coverage@0.2.3': {} + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@balena/dockerignore@1.0.2': {} + + '@bcoe/v8-coverage@1.0.2': {} '@borewit/text-codec@0.1.1': {} '@drizzle-team/brocli@0.10.2': {} - '@elysiajs/cors@1.3.3(elysia@1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2))': + '@elysiajs/cors@1.3.3(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2))': dependencies: - elysia: 1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2) + elysia: 1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) - '@elysiajs/node@1.3.0(elysia@1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2))(hono@4.9.8)': + '@elysiajs/node@1.3.0(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2))(hono@4.9.8)': dependencies: '@hono/node-server': 1.19.3(hono@4.9.8) - elysia: 1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2) + elysia: 1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) transitivePeerDependencies: - hono + '@elysiajs/openapi@1.4.11(elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2))': + dependencies: + elysia: 1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -3773,181 +3662,37 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@hono/node-server@1.19.3(hono@4.9.8)': - dependencies: - hono: 4.9.8 - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@24.5.2) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 24.5.2 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 24.5.2 - chalk: 4.1.2 - collect-v8-coverage: 1.0.2 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - - '@jest/source-map@29.6.3': + '@grpc/grpc-js@1.14.1': dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 - '@jest/test-result@29.7.0': + '@grpc/proto-loader@0.7.15': dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 - '@jest/test-sequencer@29.7.0': + '@grpc/proto-loader@0.8.0': dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 - '@jest/transform@29.7.0': + '@hono/node-server@1.19.3(hono@4.9.8)': dependencies: - '@babel/core': 7.28.4 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color + hono: 4.9.8 - '@jest/types@29.6.3': + '@isaacs/cliui@8.0.2': dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 24.5.2 - '@types/yargs': 17.0.33 - chalk: 4.1.2 + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -3968,6 +3713,34 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rollup/plugin-typescript@12.1.4(rollup@4.52.0)(tslib@2.8.1)(typescript@5.9.2)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.52.0) @@ -4051,18 +3824,18 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.0': optional: true - '@sinclair/typebox@0.27.8': {} - - '@sinclair/typebox@0.34.41': - optional: true + '@sinclair/typebox@0.34.41': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 + '@standard-schema/spec@1.0.0': {} - '@sinonjs/fake-timers@10.3.0': + '@testcontainers/postgresql@11.8.0': dependencies: - '@sinonjs/commons': 3.0.1 + testcontainers: 11.8.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color '@tokenizer/inflate@0.2.7': dependencies: @@ -4074,35 +3847,32 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@types/babel__core@7.20.5': + '@types/body-parser@1.19.6': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@types/connect': 3.4.38 + '@types/node': 24.5.2 - '@types/babel__generator@7.27.0': + '@types/chai@5.2.3': dependencies: - '@babel/types': 7.28.4 + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - '@types/babel__template@7.4.4': + '@types/connect@3.4.38': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@types/node': 24.5.2 - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.28.4 + '@types/deep-eql@4.0.2': {} - '@types/body-parser@1.19.6': + '@types/docker-modem@3.0.6': dependencies: - '@types/connect': 3.4.38 '@types/node': 24.5.2 + '@types/ssh2': 1.15.5 - '@types/connect@3.4.38': + '@types/dockerode@3.3.45': dependencies: + '@types/docker-modem': 3.0.6 '@types/node': 24.5.2 + '@types/ssh2': 1.15.5 '@types/estree@1.0.8': {} @@ -4119,28 +3889,20 @@ snapshots: '@types/express-serve-static-core': 5.0.7 '@types/serve-static': 1.15.8 - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 24.5.2 - '@types/http-errors@2.0.5': {} - '@types/istanbul-lib-coverage@2.0.6': {} + '@types/luxon@3.7.1': {} - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 + '@types/mime@1.3.5': {} - '@types/istanbul-reports@3.0.4': + '@types/node@18.19.130': dependencies: - '@types/istanbul-lib-report': 3.0.3 + undici-types: 5.26.5 - '@types/jest@29.5.14': + '@types/node@24.10.0': dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/mime@1.3.5': {} + undici-types: 7.16.0 + optional: true '@types/node@24.5.2': dependencies: @@ -4172,39 +3934,132 @@ snapshots: '@types/node': 24.5.2 '@types/send': 0.17.5 - '@types/stack-utils@2.0.3': {} + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 24.5.2 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 24.5.2 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + + '@vitest/coverage-v8@4.0.8(vitest@4.0.8(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.8 + ast-v8-to-istanbul: 0.3.8 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.8(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.0.8': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.8 + '@vitest/utils': 4.0.8 + chai: 6.2.0 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + + '@vitest/pretty-format@4.0.8': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.8': + dependencies: + '@vitest/utils': 4.0.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.8': + dependencies: + '@vitest/pretty-format': 4.0.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.8': {} - '@types/yargs-parser@21.0.3': {} + '@vitest/utils@4.0.8': + dependencies: + '@vitest/pretty-format': 4.0.8 + tinyrainbow: 3.0.3 - '@types/yargs@17.0.33': + abort-controller@3.0.0: dependencies: - '@types/yargs-parser': 21.0.3 + event-target-shim: 5.0.1 accepts@2.0.0: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} - anymatch@3.1.3: + archiver-utils@5.0.2: dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 normalize-path: 3.0.0 - picomatch: 2.3.1 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a - argparse@1.0.10: + asn1@0.2.6: dependencies: - sprintf-js: 1.0.3 + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.8: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + + async-lock@1.4.1: {} + + async@3.2.6: {} asynckit@0.4.0: {} @@ -4216,89 +4071,85 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@29.7.0(@babel/core@7.28.4): + b4a@1.7.3: {} + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): dependencies: + '@babel/compat-data': 7.28.4 '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.4) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@6.1.1: + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): - dependencies: - '@babel/compat-data': 7.28.4 '@babel/core': 7.28.4 '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) - semver: 6.3.1 + core-js-compat: 3.45.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): dependencies: '@babel/core': 7.28.4 '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) - core-js-compat: 3.45.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): + balanced-match@1.0.2: {} + + bare-events@2.8.2: {} + + bare-fs@4.5.0: dependencies: - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 transitivePeerDependencies: - - supports-color + - bare-abort-controller + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): + bare-path@3.0.0: dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - - babel-preset-jest@29.6.3(@babel/core@7.28.4): + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.8.2): dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + streamx: 2.23.0 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true - balanced-match@1.0.2: {} + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + optional: true + + base64-js@1.5.1: {} baseline-browser-mapping@2.8.6: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -4315,14 +4166,9 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.12: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 browserslist@4.26.2: dependencies: @@ -4332,22 +4178,31 @@ snapshots: node-releases: 2.0.21 update-browserslist-db: 1.1.3(browserslist@4.26.2) - bs-logger@0.2.6: + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: dependencies: - fast-json-stable-stringify: 2.1.0 + base64-js: 1.5.1 + ieee754: 1.2.1 - bser@2.1.1: + buffer@6.0.3: dependencies: - node-int64: 0.4.0 + base64-js: 1.5.1 + ieee754: 1.2.1 - buffer-from@1.1.2: {} + buildcheck@0.0.6: + optional: true bun-types@1.2.22(@types/react@19.1.13): dependencies: - '@types/node': 24.5.2 + '@types/node': 24.10.0 '@types/react': 19.1.13 optional: true + byline@5.0.0: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -4360,20 +4215,9 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - caniuse-lite@1.0.30001743: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} + chai@6.2.0: {} cheerio-select@2.1.0: dependencies: @@ -4394,9 +4238,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} + chownr@1.1.4: {} cliui@8.0.1: dependencies: @@ -4404,10 +4246,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - co@4.6.0: {} - - collect-v8-coverage@1.0.2: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4418,7 +4256,13 @@ snapshots: dependencies: delayed-stream: 1.0.0 - concat-map@0.0.1: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 content-disposition@1.0.0: dependencies: @@ -4438,20 +4282,20 @@ snapshots: dependencies: browserslist: 4.26.2 - create-jest@29.7.0(@types/node@24.5.2): + core-util-is@1.0.3: {} + + cpu-features@0.0.10: dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.5.2) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + buildcheck: 0.0.6 + nan: 2.23.1 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 cross-spawn@7.0.6: dependencies: @@ -4476,17 +4320,34 @@ snapshots: dependencies: ms: 2.1.3 - dedent@1.7.0: {} - - deepmerge@4.3.1: {} - delayed-stream@1.0.0: {} depd@2.0.0: {} - detect-newline@3.1.0: {} + docker-compose@1.3.0: + dependencies: + yaml: 2.8.1 + + docker-modem@5.0.6: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color - diff-sequences@29.6.3: {} + dockerode@4.0.9: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.1 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.6 + protobufjs: 7.5.4 + tar-fs: 2.1.4 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color dom-serializer@2.0.0: dependencies: @@ -4540,39 +4401,44 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} electron-to-chromium@1.5.222: {} - elysia@1.3.5(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2): + elysia@1.4.15(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2): dependencies: + '@sinclair/typebox': 0.34.41 cookie: 1.0.2 exact-mirror: 0.2.2(@sinclair/typebox@0.34.41) fast-decode-uri-component: 1.0.1 file-type: 21.0.0 - typescript: 5.9.2 - optionalDependencies: - '@sinclair/typebox': 0.34.41 + memoirist: 0.4.0 openapi-types: 12.1.3 - - emittery@0.13.1: {} + optionalDependencies: + typescript: 5.9.2 emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@4.5.0: {} entities@6.0.1: {} - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -4649,41 +4515,31 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@2.0.0: {} - - esprima@4.0.1: {} - estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} - exact-mirror@0.2.2(@sinclair/typebox@0.34.41): - optionalDependencies: - '@sinclair/typebox': 0.34.41 + event-target-shim@5.0.1: {} - execa@5.1.1: + events-universal@1.0.1: dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller - exit@0.1.2: {} + events@3.3.0: {} - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + exact-mirror@0.2.2(@sinclair/typebox@0.34.41): + optionalDependencies: + '@sinclair/typebox': 0.34.41 + + expect-type@1.2.2: {} express@5.1.0: dependencies: @@ -4719,11 +4575,11 @@ snapshots: fast-decode-uri-component@1.0.1: {} - fast-json-stable-stringify@2.1.0: {} + fast-fifo@1.3.2: {} - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 fflate@0.8.2: {} @@ -4736,10 +4592,6 @@ snapshots: transitivePeerDependencies: - supports-color - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - finalhandler@2.1.0: dependencies: debug: 4.4.3 @@ -4751,13 +4603,13 @@ snapshots: transitivePeerDependencies: - supports-color - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - follow-redirects@1.15.11: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.4: dependencies: asynckit: 0.4.0 @@ -4770,7 +4622,7 @@ snapshots: fresh@2.0.0: {} - fs.realpath@1.0.0: {} + fs-constants@1.0.0: {} fsevents@2.3.3: optional: true @@ -4794,41 +4646,32 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 - get-package-type@0.1.0: {} + get-port@7.1.0: {} get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@6.0.1: {} - get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 - glob@7.2.3: + glob@10.4.5: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globrex@0.1.2: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4860,8 +4703,6 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - human-signals@2.1.0: {} - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -4872,73 +4713,37 @@ snapshots: ieee754@1.2.1: {} - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} ipaddr.js@1.9.1: {} - is-arrayish@0.2.1: {} - is-core-module@2.16.1: dependencies: hasown: 2.0.2 is-fullwidth-code-point@3.0.0: {} - is-generator-fn@2.1.0: {} - - is-number@7.0.0: {} - is-promise@4.0.0: {} is-stream@2.0.1: {} + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: + istanbul-lib-source-maps@5.0.6: dependencies: + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -4947,369 +4752,63 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.0 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@24.5.2): - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.5.2) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.5.2) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@24.5.2): - dependencies: - '@babel/core': 7.28.4 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 24.5.2 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: + jackspeak@3.4.3: dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 24.5.2 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + '@isaacs/cliui': 8.0.2 optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.10 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.4 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.7.2 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 24.5.2 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 24.5.2 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@24.5.2): - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.5.2) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + '@pkgjs/parseargs': 0.11.0 js-tokens@4.0.0: {} - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + js-tokens@9.0.1: {} jsesc@3.0.2: {} jsesc@3.1.0: {} - json-parse-even-better-errors@2.3.1: {} - json5@2.2.3: {} - kleur@3.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 - leven@3.1.0: {} + lodash.camelcase@4.3.0: {} - lines-and-columns@1.2.4: {} + lodash.debounce@4.0.8: {} - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 + lodash@4.17.21: {} - lodash.debounce@4.0.8: {} + long@5.3.2: {} - lodash.memoize@4.1.2: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - make-dir@4.0.0: + luxon@3.7.2: {} + + magic-string@0.30.21: dependencies: - semver: 7.7.2 + '@jridgewell/sourcemap-codec': 1.5.5 - make-error@1.3.6: {} + magicast@0.5.1: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + source-map-js: 1.2.1 - makeerror@1.0.12: + make-dir@4.0.0: dependencies: - tmpl: 1.0.5 + semver: 7.7.3 math-intrinsics@1.1.0: {} media-typer@1.1.0: {} - merge-descriptors@2.0.0: {} + memoirist@0.4.0: {} - merge-stream@2.0.0: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + merge-descriptors@2.0.0: {} mime-db@1.52.0: {} @@ -5323,23 +4822,30 @@ snapshots: dependencies: mime-db: 1.54.0 - mimic-fn@2.1.0: {} + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 - minimatch@3.1.2: + minimatch@9.0.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 2.0.2 minimist@1.2.8: {} - ms@2.1.3: {} + minipass@7.1.2: {} - natural-compare@1.4.0: {} + mkdirp-classic@0.5.3: {} - negotiator@1.0.0: {} + mkdirp@1.0.4: {} - neo-async@2.6.2: {} + ms@2.1.3: {} + + nan@2.23.1: + optional: true - node-int64@0.4.0: {} + nanoid@3.3.11: {} + + negotiator@1.0.0: {} node-releases@2.0.21: {} @@ -5347,10 +4853,6 @@ snapshots: npm-check-updates@18.3.0: {} - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -5365,33 +4867,9 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - openapi-types@12.1.3: - optional: true - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 + openapi-types@12.1.3: {} - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-try@2.2.0: {} - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 + package-json-from-dist@1.0.1: {} parse5-htmlparser2-tree-adapter@7.1.0: dependencies: @@ -5404,16 +4882,19 @@ snapshots: parseurl@1.3.3: {} - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + pg-cloudflare@1.2.7: optional: true @@ -5451,15 +4932,13 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.3: {} - pirates@4.0.7: {} - - pkg-dir@4.2.0: + postcss@8.5.6: dependencies: - find-up: 4.1.0 + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 postgres-array@2.0.0: {} @@ -5471,16 +4950,34 @@ snapshots: dependencies: xtend: 4.0.2 - pretty-format@29.7.0: + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + proper-lockfile@4.1.2: dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 - prompts@2.4.2: + properties-reader@2.3.0: dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 + mkdirp: 1.0.4 + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.5.2 + long: 5.3.2 proxy-addr@2.0.7: dependencies: @@ -5489,7 +4986,10 @@ snapshots: proxy-from-env@1.1.0: {} - pure-rand@6.1.0: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 qs@6.14.0: dependencies: @@ -5504,7 +5004,33 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 - react-is@18.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 regenerate-unicode-properties@10.2.2: dependencies: @@ -5529,22 +5055,16 @@ snapshots: require-directory@2.1.1: {} - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.3: {} - resolve@1.22.10: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + rollup@4.52.0: dependencies: '@types/estree': 1.0.8 @@ -5583,13 +5103,15 @@ snapshots: transitivePeerDependencies: - supports-color + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} semver@6.3.1: {} - semver@7.7.2: {} + semver@7.7.3: {} send@1.2.0: dependencies: @@ -5652,16 +5174,13 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} - sisteransi@1.0.5: {} + signal-exit@4.1.0: {} - slash@3.0.0: {} - - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: @@ -5670,22 +5189,39 @@ snapshots: source-map@0.6.1: {} + split-ca@1.0.1: {} + split2@4.2.0: {} - sprintf-js@1.0.3: {} + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 - stack-utils@2.0.6: + ssh2@1.17.0: dependencies: - escape-string-regexp: 2.0.0 + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.23.1 + + stackback@0.0.2: {} statuses@2.0.1: {} statuses@2.0.2: {} - string-length@4.0.2: + std-env@3.10.0: {} + + streamx@2.23.0: dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a string-width@4.2.3: dependencies: @@ -5693,15 +5229,27 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - strip-ansi@6.0.1: + string-width@5.1.2: dependencies: - ansi-regex: 5.0.1 + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 - strip-bom@4.0.0: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 - strip-final-newline@2.0.0: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 - strip-json-comments@3.1.1: {} + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 strtok3@10.3.4: dependencies: @@ -5711,23 +5259,85 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: + supports-preserve-symlinks-flag@1.0.0: {} + + tar-fs@2.1.4: dependencies: - has-flag: 4.0.0 + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 - supports-preserve-symlinks-flag@1.0.0: {} + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.5.0 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + testcontainers@11.8.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 3.3.45 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 1.3.0 + dockerode: 4.0.9 + get-port: 7.1.0 + proper-lockfile: 4.1.2 + properties-reader: 2.3.0 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.1 + tmp: 0.2.5 + undici: 7.16.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color - test-exclude@6.0.0: + text-decoder@1.2.3: dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + + tinybench@2.9.0: {} - tmpl@1.0.5: {} + tinyexec@0.3.2: {} - to-regex-range@5.0.1: + tinyglobby@0.2.15: dependencies: - is-number: 7.0.0 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tmp@0.2.5: {} toidentifier@1.0.1: {} @@ -5737,26 +5347,9 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - ts-jest@29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@24.5.2))(typescript@5.9.2): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@24.5.2) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.2 - type-fest: 4.41.0 - typescript: 5.9.2 - yargs-parser: 21.1.1 + tsconfck@3.1.6(typescript@5.9.2): optionalDependencies: - '@babel/core': 7.28.4 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.4) - esbuild: 0.25.10 - jest-util: 29.7.0 + typescript: 5.9.2 tslib@2.8.1: {} @@ -5767,11 +5360,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-detect@4.0.8: {} - - type-fest@0.21.3: {} - - type-fest@4.41.0: {} + tweetnacl@0.14.5: {} type-is@2.0.1: dependencies: @@ -5781,13 +5370,17 @@ snapshots: typescript@5.9.2: {} - uglify-js@3.19.3: - optional: true - uint8array-extras@1.5.0: {} + undici-types@5.26.5: {} + undici-types@7.12.0: {} + undici-types@7.16.0: + optional: true + + undici@7.16.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -5807,23 +5400,83 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 + util-deprecate@1.0.2: {} + + uuid@10.0.0: {} vary@1.1.2: {} - walker@1.0.8: + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)): dependencies: - makeerror: 1.0.12 + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.2) + optionalDependencies: + vite: 7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.5.2 + fsevents: 2.3.3 + tsx: 4.20.5 + yaml: 2.8.1 + + vitest@4.0.8(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.8 + '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.8 + '@vitest/runner': 4.0.8 + '@vitest/snapshot': 4.0.8 + '@vitest/spy': 4.0.8 + '@vitest/utils': 4.0.8 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.2(@types/node@24.5.2)(tsx@4.20.5)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.5.2 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml which@2.0.2: dependencies: isexe: 2.0.0 - wordwrap@1.0.0: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 wrap-ansi@7.0.0: dependencies: @@ -5831,12 +5484,13 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: + wrap-ansi@8.1.0: dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} xtend@4.0.2: {} @@ -5844,6 +5498,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.1: {} + yargs-parser@21.1.1: {} yargs@17.7.2: @@ -5856,6 +5512,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yocto-queue@0.1.0: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 zod@3.25.76: {} diff --git a/src/containers/locationBuilder.ts b/src/containers/locationBuilder.ts index b67a1a8..a888a94 100644 --- a/src/containers/locationBuilder.ts +++ b/src/containers/locationBuilder.ts @@ -1,9 +1,14 @@ import { getHTMLResponse } from "utils/requestUtils"; -import { load } from "cheerio"; -import type { Element } from "domhandler"; -import { getTimeRangesFromString } from "./timeBuilder"; -import { ICoordinate, ILocation, ISpecial, ITimeRange } from "../types"; -import { sortAndMergeTimeRanges } from "utils/timeUtils"; +import { Element, load } from "cheerio"; +import { getAllTimeSlotsFromSchedule } from "./timeBuilder"; +import { + ICoordinate, + IDate, + ILocation, + ISpecial, + IFullTimeRange, +} from "../types"; +import { notifySlack } from "utils/slack"; /** * For building the location data structure @@ -12,18 +17,19 @@ export default class LocationBuilder { static readonly CONCEPT_BASE_LINK = "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/"; - private conceptId?: number; - private name?: string; - private shortDescription?: string; - private description?: string; - private url?: string; - private location?: string; - private menu?: string; - private coordinates?: ICoordinate; - private acceptsOnlineOrders?: boolean; - private times?: ITimeRange[]; - private specials?: ISpecial[]; - private soups?: ISpecial[]; + private conceptId: number | undefined; + private name: string | undefined; + private shortDescription: string | undefined; + private description: string | undefined; + private url: string | undefined; + private location: string | undefined; + private menu: string | undefined; + private coordinates: ICoordinate | undefined; + private acceptsOnlineOrders: boolean | undefined; + private times: IFullTimeRange[] | undefined; + private today: IDate | undefined; + private specials: ISpecial[] | undefined; + private soups: ISpecial[] | undefined; constructor(card: Element) { const link = load(card)("h3.name.detailsLink"); @@ -48,7 +54,7 @@ export default class LocationBuilder { this.specials = specialList[this.conceptId]; } } - setTimes(times: ITimeRange[]) { + setTimes(times: IFullTimeRange[]) { if (this.conceptId && times !== undefined) { this.times = times; } @@ -66,7 +72,8 @@ export default class LocationBuilder { const conceptURL = this.getConceptLink(); if (!conceptURL) return; - const $ = load(await getHTMLResponse(conceptURL)); + const { body, serverDate } = await getHTMLResponse(conceptURL); + const $ = load(body); this.url = conceptURL.toString(); this.description = $("div.description p").text().trim(); this.menu = $("div.navItems > a#getMenu").attr("href"); @@ -80,9 +87,12 @@ export default class LocationBuilder { } const nextSevenDays = $("ul.schedule").find("li").toArray(); - this.times = sortAndMergeTimeRanges( - nextSevenDays.flatMap((rowHTML) => getTimeRangesFromString(rowHTML)) + const { times, earliestDay } = getAllTimeSlotsFromSchedule( + nextSevenDays, + serverDate.year ); + this.times = times; + this.today = earliestDay; } getConceptLink() { if (this.conceptId === undefined) return undefined; @@ -90,11 +100,10 @@ export default class LocationBuilder { } getConceptId() { - if (this.conceptId === undefined) return undefined; return this.conceptId; } - build(): ILocation { + build(): ILocation | undefined { if ( this.times === undefined || this.acceptsOnlineOrders === undefined || @@ -102,16 +111,15 @@ export default class LocationBuilder { this.url === undefined || this.location === undefined || this.conceptId === undefined || - this.name === undefined + this.name === undefined || + this.today === undefined ) { - throw Error( - "Didn't finish configuring location before building metadata!" + notifySlack( + ` Didn't finish configuring location for ${this.conceptId} before building metadata!` ); + return undefined; // All fetches were good - yet we have missing data. This is a problem. } - if (this.conceptId === 179) { - this.times = []; // capital grains quick override - } return { conceptId: this.conceptId, @@ -124,6 +132,7 @@ export default class LocationBuilder { coordinates: this.coordinates, acceptsOnlineOrders: this.acceptsOnlineOrders, times: this.times, + today: this.today, todaysSpecials: this.specials, todaysSoups: this.soups, }; diff --git a/src/containers/time/parsedTime.ts b/src/containers/time/parsedTime.ts index b368be8..d7389de 100644 --- a/src/containers/time/parsedTime.ts +++ b/src/containers/time/parsedTime.ts @@ -5,6 +5,7 @@ interface Time { minute: number; } +/** what you get after parsing a time string like `2:00 AM - 3:00 AM, 4:00 PM - 2:00 AM` (do note that end can be < start) */ export interface IParsedTimeRange { start: Time; end: Time; @@ -23,15 +24,15 @@ export default class ParsedTime extends ParsedTimeBase { if (tokens.length !== 3) { throw new Error(`Invalid time ${timeStr}`); } - const hour = parseInt(tokens[0]); + const hour = parseInt(tokens[0]!); if (!Number.isInteger(hour) || hour > 12 || hour < 1) { throw new Error(`Invalid time ${timeStr}`); } - const minute = parseInt(tokens[1]); + const minute = parseInt(tokens[1]!); if (!Number.isInteger(minute) || minute > 59 || minute < 0) { throw new Error(`Invalid time ${timeStr}`); } - if (!["am", "pm"].includes(tokens[2])) { + if (!["am", "pm"].includes(tokens[2]!)) { throw new Error(`Invalid time ${timeStr}`); } if (tokens[2] === "am") { @@ -52,8 +53,8 @@ export default class ParsedTime extends ParsedTimeBase { if (tokens.length !== 2) { throw new Error(`Invalid time range: ${this.input}`); } - const start = this.parseTime(tokens[0]); - const end = this.parseTime(tokens[1]); + const start = this.parseTime(tokens[0]!); + const end = this.parseTime(tokens[1]!); this.value = { start, end, diff --git a/src/containers/time/parsedTimeForDate.ts b/src/containers/time/parsedTimeForDate.ts index 787d783..8c37280 100644 --- a/src/containers/time/parsedTimeForDate.ts +++ b/src/containers/time/parsedTimeForDate.ts @@ -2,7 +2,9 @@ import { MonthOfTheYear } from "types"; import ParsedTimeBase from "./parsedTimeBase"; export interface IParsedTimeDate { + /** 1-12 */ month: MonthOfTheYear; + /* 1-31 */ date: number; } @@ -17,8 +19,8 @@ export default class ParsedTimeForDate extends ParsedTimeBase { if (tokens.length < 2) { throw new Error(`Invalid date: ${this.input}`); } - const month = convertMonthStringToEnum(tokens[0]); - const date = parseInt(tokens[1]); + const month = convertMonthStringToEnum(tokens[0]!); + const date = parseInt(tokens[1]!); if (!Number.isInteger(date)) { throw new Error(`Invalid date: ${this.input}`); @@ -36,6 +38,11 @@ export default class ParsedTimeForDate extends ParsedTimeBase { } } +/** + * Throws error when failed + * @param monthStr + * @returns + */ export function convertMonthStringToEnum(monthStr: string): MonthOfTheYear { const normalizedMonth = monthStr.trim().toLowerCase(); switch (normalizedMonth) { diff --git a/src/containers/time/parsedTimeForDay.ts b/src/containers/time/parsedTimeForDay.ts index e729a1e..e0dd808 100644 --- a/src/containers/time/parsedTimeForDay.ts +++ b/src/containers/time/parsedTimeForDay.ts @@ -1,11 +1,10 @@ -import { DayOfTheWeek } from "types"; import ParsedTimeBase from "./parsedTimeBase"; /** - * For parsing a string representing a day to a day of the week enum + * For parsing a string representing a day to a day of the week (0-6) */ export default class ParsedTimeForDay extends ParsedTimeBase { - declare value: DayOfTheWeek; + declare value: number; parse() { this.value = convertDayStringToEnum(this.input); @@ -13,31 +12,31 @@ export default class ParsedTimeForDay extends ParsedTimeBase { } } -export function convertDayStringToEnum(dayStr: string): DayOfTheWeek { +export function convertDayStringToEnum(dayStr: string): number { const normalizedDay = dayStr.trim().toLowerCase(); switch (normalizedDay) { case "sunday": case "sun": - return DayOfTheWeek.SUNDAY; + return 0; case "monday": case "mon": - return DayOfTheWeek.MONDAY; + return 1; case "tuesday": case "tue": - return DayOfTheWeek.TUESDAY; + return 2; case "wednesday": case "wed": - return DayOfTheWeek.WEDNESDAY; + return 3; case "thursday": case "thu": case "thurs": - return DayOfTheWeek.THURSDAY; + return 4; case "friday": case "fri": - return DayOfTheWeek.FRIDAY; + return 5; case "saturday": case "sat": - return DayOfTheWeek.SATURDAY; + return 6; default: throw new Error(`Invalid Day: ${dayStr}`); } diff --git a/src/containers/timeBuilder.ts b/src/containers/timeBuilder.ts index 8f04347..7f03ebf 100644 --- a/src/containers/timeBuilder.ts +++ b/src/containers/timeBuilder.ts @@ -1,145 +1,138 @@ -import { load } from "cheerio"; -import type { Element } from "domhandler"; +import { Element, load } from "cheerio"; -import { getNextDay } from "../utils/timeUtils"; import { IParsedTimeRange } from "./time/parsedTime"; -import { IParsedTimeDate } from "./time/parsedTimeForDate"; -import { DayOfTheWeek, ITimeRange, TimeInfoType } from "types"; +import { convertMonthStringToEnum } from "./time/parsedTimeForDate"; +import { IDate, IFullTimeRange, TimeInfoType } from "types"; import { parseToken } from "utils/parseTimeToken"; import { notifySlack } from "utils/slack"; - -interface ITimeRowAttributes { - day?: DayOfTheWeek; - date?: IParsedTimeDate; - /** Multiple times in the same day (ex. https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/180) */ - times?: IParsedTimeRange[]; - closed?: boolean; - twentyFour?: boolean; -} +import { DateTime } from "luxon"; /** * - * @param rowString ex. Monday, September 09, 7:30 AM - 10:00 AM, 11:00 AM - 2:00 PM, 4:30 PM - 8:30 PM + * @param schedule + * @param currentYear Current year, XXXX + * @returns */ -export function getTimeRangesFromString(rowHTML: Element) { - let timeRowInfo: ITimeRowAttributes = getTimeAttributesFromRow(rowHTML); - timeRowInfo = resolveAttributeConflicts(timeRowInfo); - return getTimeRangesFromTimeRow(timeRowInfo); -} +export function getAllTimeSlotsFromSchedule( + schedule: Element[], + currentYear: number +) { + const allTimeSlots: IFullTimeRange[] = []; + let prevMonth = -1; + let firstDay: IDate | undefined; + for (const rowHTML of schedule) { + const $ = load(rowHTML); + try { + const [date, timeSlotsForThatDay] = $.text() + .replaceAll("\n", "") + .replace(/\s\s+/g, " ") + .split(/,(.*)/); // splits only on first comma + const [dayOfWeek, month, day] = date?.trim().split(" ") ?? []; + if ( + dayOfWeek === undefined || + month === undefined || + day === undefined || + date === undefined || + timeSlotsForThatDay === undefined + ) + continue; + const parsedMonth = convertMonthStringToEnum(month); + const parsedDay = parseInt(day); -function getTimeAttributesFromRow(rowHTML: Element) { - return getTimeInfoWithRawAttributes(tokenizeTimeRow(rowHTML)); -} + if (parsedMonth < prevMonth) { + // new year + currentYear++; + } + prevMonth = parsedMonth; -function tokenizeTimeRow(rowHTML: Element) { - const $ = load(rowHTML); - let day = $("strong").text(); - const dataStr = $.text().replace(/\s\s+/g, " ").replace(day, "").trim(); - let [date, time] = dataStr.split(/,(.+)/); - if (date === undefined || time === undefined) return []; + const rowFullDateString = `${parsedMonth}/${parsedDay}/${currentYear}`; + const rowDate = DateTime.fromFormat(rowFullDateString, "M/d/y"); - day = (day.charAt(0).toUpperCase() + day.slice(1).toLowerCase()).trim(); - date = (date.charAt(0).toUpperCase() + date.slice(1).toLowerCase()).trim(); - time = time.toUpperCase().trim(); - const timeSlots = time.split(/[,;]/).map((slot) => slot.trim()); - return [day, date, ...timeSlots]; -} + if (!rowDate.isValid) { + notifySlack( + ` Cannot parse date ${rowFullDateString}, derived from ${$.text()}` + ); + continue; + } + const parsedTimeSlots = parseTimeSlots(timeSlotsForThatDay); + const parsedTimeSlotsWithDateInfo = augmentAndEditTimeRangesWithDateInfo( + parsedTimeSlots, + rowDate.day, + rowDate.month, + rowDate.year + ); -function getTimeInfoWithRawAttributes(tokens: string[]) { - const timeInfo: ITimeRowAttributes = {}; + allTimeSlots.push(...parsedTimeSlotsWithDateInfo); + if (firstDay === undefined) + firstDay = { + year: rowDate.year, + month: rowDate.month, + day: rowDate.day, + }; + } catch (e) { + notifySlack( + ` Failed to parse row ${$.text()} ${e} ${(e as any).stack}` + ); + } + } + if (firstDay === undefined) throw new Error("No valid rows parsed!"); + return { times: allTimeSlots, earliestDay: firstDay }; +} +/** + * + * @param timeString The actual time slots (ex. '10:30 AM - 8:00 PM' in the string 'Tuesday September 09, 10:30 AM - 8:00 PM') + * @returns + */ +export function parseTimeSlots(timeSlotStrings: string) { + const timeRanges: IParsedTimeRange[] = []; - for (const token of tokens) { + for (const token of timeSlotStrings + .split(/[,;]/) + .map((slot) => slot.trim())) { try { const { type: timeInfoType, value } = parseToken(token); switch (timeInfoType) { - case TimeInfoType.DAY: - timeInfo.day = value; - break; - case TimeInfoType.DATE: - timeInfo.date = value; - break; case TimeInfoType.TIME: - if (timeInfo.times !== undefined) { - timeInfo.times.push(value); - } else { - timeInfo.times = [value]; - } - break; - case TimeInfoType.CLOSED: - timeInfo.closed = true; + timeRanges.push(value); break; case TimeInfoType.TWENTYFOURHOURS: - timeInfo.twentyFour = true; - break; + timeRanges.push({ + start: { hour: 0, minute: 0 }, + end: { hour: 23, minute: 59 }, + }); } } catch (err: any) { notifySlack( - ` Failed to parse token \`${token}\` from list of tokens \`${tokens}\`\n${err.stack}` + ` Failed to parse token \`${token}\` from time slot \`${timeSlotStrings}\`\n${err.stack}` ); continue; } } - return timeInfo; + return timeRanges; } -function resolveAttributeConflicts( - input: ITimeRowAttributes -): ITimeRowAttributes { - if (input.closed) { - return { - day: input.day, - date: input.date, - closed: input.closed, - }; - } - if (input.twentyFour) { - return { - day: input.day, - date: input.date, - times: [{ start: { hour: 0, minute: 0 }, end: { hour: 23, minute: 59 } }], - }; - } - if (input.times && input.times.length > 0) { - return { - day: input.day, - date: input.date, - times: input.times, - }; - } - return { - day: input.day, - date: input.date, - times: [], - }; -} - -function getTimeRangesFromTimeRow(time: ITimeRowAttributes) { - if (time.day === undefined) { - notifySlack( - ` Cannot convert time attribute: ${JSON.stringify( - time - )} since day is not set` - ); - return []; - } - const allRanges: ITimeRange[] = []; - for (const range of time.times ?? []) { +/** + * + * @param timeRanges + * @param day (0-6, with Sunday being 0) + * @returns + */ +export function augmentAndEditTimeRangesWithDateInfo( + timeRanges: IParsedTimeRange[], + day: number, + month: number, + year: number +) { + const allRanges: IFullTimeRange[] = []; + for (const range of timeRanges) { rollBack12AmEndTime(range); // not sure why this was added, but it doesn't hurt I guess (I suppose the only case this actively helps is if the time string is 12:00 AM - 12:00 AM) - const shouldSpillToNextDay = - range.start.hour * 60 + range.start.minute > - range.end.hour * 60 + range.end.minute; allRanges.push({ - start: { - day: time.day, - hour: range.start.hour, - minute: range.start.minute, - }, - end: { - day: shouldSpillToNextDay ? getNextDay(time.day) : time.day, - hour: range.end.hour, - minute: range.end.minute, - }, + year: year, + month: month, + day: day, + startMinutesFromMidnight: range.start.hour * 60 + range.start.minute, + endMinutesFromMidnight: range.end.hour * 60 + range.end.minute, }); } return allRanges; diff --git a/src/db/db.ts b/src/db/db.ts index 558c7bb..87c0c77 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,13 +1,20 @@ -import { drizzle } from "drizzle-orm/node-postgres"; +import { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; -import { env } from "env"; import * as schema from "./schema"; -const pool: Pool = new Pool({ - connectionString: env.DATABASE_URL, - ssl: false, -}); +export type DBType = NodePgDatabase & { + $client: Pool; +}; -export const db = drizzle(pool, { - schema, -}); +export function initDBConnection(connectionString: string) { + const pool = new Pool({ + connectionString: connectionString, + ssl: false, + }); + return [ + pool, + drizzle(pool, { + schema, + }), + ] as const; +} diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts new file mode 100644 index 0000000..db91230 --- /dev/null +++ b/src/db/dbQueryUtils.ts @@ -0,0 +1,168 @@ +import { + conceptIdToInternalIdTable, + emailTable, + locationDataTable, + overwritesTable, + specialsTable, + timeOverwritesTable, + timesTable, +} from "./schema"; + +import { DBType } from "./db"; +import { notifySlack } from "utils/slack"; +import { and, eq, gte } from "drizzle-orm"; +import { parseTimeSlots } from "containers/timeBuilder"; +import { IParsedTimeRange } from "containers/time/parsedTime"; + +type RequiredProperty = { [P in keyof T]: NonNullable }; + +/** More-so the database representation of a time range */ +export interface ITimeRangeInternal { + date: string; + startMinutesSinceMidnight: number; + endMinutesSinceMidnight: number; +} +export class QueryUtils { + db: DBType; + constructor(db: DBType) { + this.db = db; + } + + async getSpecials(todayAsSQLString: string) { + const data = await this.db + .select() + .from(specialsTable) + .where(eq(specialsTable.date, todayAsSQLString)); + return data.reduce< + Record< + string, + { + specials?: { name: string; description: string }[]; + soups?: { name: string; description: string }[]; + } + > + >((acc, special) => { + if (acc[special.locationId] === undefined) acc[special.locationId] = {}; + if (special.type === "special") { + acc[special.locationId]!.specials = [ + ...(acc[special.locationId]!.specials ?? []), + { + name: special.name, + description: special.description, + }, + ]; + } else { + acc[special.locationId]!.soups = [ + ...(acc[special.locationId]!.soups ?? []), + { + name: special.name, + description: special.description, + }, + ]; + } + return acc; + }, {}); + } + /** Fetches non-overridden location data + open times */ + async getLocationIdToDataMap(timeSearchCutoffStr: string) { + const locationData = await this.db + .select() + .from(locationDataTable) + .leftJoin( + timesTable, + and( + eq(locationDataTable.id, timesTable.locationId), + gte(timesTable.date, timeSearchCutoffStr) + ) + ); + return locationData.reduce< + Record< + string, + | typeof locationDataTable.$inferSelect & { + times: ITimeRangeInternal[]; + } + > + >((acc, { location_data, location_times }) => { + if (!acc[location_data.id]) { + acc[location_data.id] = { + ...location_data, + times: [], + }; + } + if (location_times !== null) { + acc[location_data.id]!.times.push({ + startMinutesSinceMidnight: location_times.startTime, + endMinutesSinceMidnight: location_times.endTime, + date: location_times.date, + }); + } + return acc; + }, {}); + } + + async getGeneralOverrides() { + return (await this.db.select().from(overwritesTable)).reduce< + Record< + string, + Omit< + RequiredProperty, + "locationId" + > // exclude locationId field, since that's our internal id + > + >( + (acc, overwrite) => ({ + ...acc, + [overwrite.locationId]: Object.fromEntries( + Object.entries(overwrite).filter(([key, v]) => { + return key !== "locationId" && v !== null; + }) + ) as RequiredProperty, + }), + {} + ); + } + + /** + * + * @param earliestDate should be in SQL form YYYY-MM-DD + */ + async getTimeOverrides(earliestDate: string) { + const timeOverrides = await this.db + .select() + .from(timeOverwritesTable) + .where(gte(timeOverwritesTable.date, earliestDate)) + .catch((e) => { + notifySlack( + ` Failed to fetch time overwrites with error ${e}` + ); + return []; + }); + const idToTimeOverrides = timeOverrides.reduce<{ + [locationId in string]: { [date in string]: IParsedTimeRange[] }; + }>((acc, override) => { + return { + ...acc, + [override.locationId]: { + ...acc[override.locationId], + [override.date]: parseTimeSlots(override.timeString), + }, + }; + }, {}); + return idToTimeOverrides; + } + + async getEmails(): Promise<{ name: string; email: string }[]> { + const result = await this.db + .select({ + name: emailTable.name, + email: emailTable.email, + }) + .from(emailTable); + + // Remove 'mailto:' if present + return result.map((row) => ({ + name: row.name, + email: row.email.replace(/^mailto:/, ""), + })); + } +} diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts new file mode 100644 index 0000000..2331242 --- /dev/null +++ b/src/db/getLocations.ts @@ -0,0 +1,64 @@ +import { ITimeRangeInternal, QueryUtils } from "./dbQueryUtils"; +import { DateTime } from "luxon"; +import { pad, remapAndMergeTimeIntervals } from "utils/timeUtils"; +import { IParsedTimeRange } from "containers/time/parsedTime"; +import { DBType } from "./db"; + +/** + * + * @param db + * @param today this parameter is necessary so we can get today's specials and the open hours for the next 7 days, rather than returning everything we've stored in the db + * @returns + */ +export async function getAllLocationsFromDB(db: DBType, today: DateTime) { + const timeSearchCutoff = today.minus({ days: 1 }); // 1 days worth of data before today + + const DB = new QueryUtils(db); + const locationIdToData = await DB.getLocationIdToDataMap( + timeSearchCutoff.toSQLDate() + ); + const specials = await DB.getSpecials(today.toSQLDate()); + const generalOverrides = await DB.getGeneralOverrides(); + const timeOverrides = await DB.getTimeOverrides(timeSearchCutoff.toSQLDate()); + + // apply overrides, merge all time intervals, and add specials + const finalLocationData = Object.entries(locationIdToData).map( + ([id, data]) => { + return { + ...data, + ...generalOverrides[id], // this line only works because the override table has the same columns as the normal table + times: remapAndMergeTimeIntervals( + applyTimeOverrides(data.times, timeOverrides[id]) + ), + // .map( + // (time) => + // `${new Date(time.start).toLocaleString()}-${new Date( + // time.end + // ).toLocaleString()}` + // ), + todaysSoups: specials[id]?.soups ?? [], + todaysSpecials: specials[id]?.specials ?? [], + }; + } + ); + return finalLocationData; +} + +function applyTimeOverrides( + originalTimes: ITimeRangeInternal[], + overrideTimes?: { [date in string]: IParsedTimeRange[] } +) { + if (overrideTimes === undefined) return originalTimes; + for (const [overrideDate, timeRanges] of Object.entries(overrideTimes)) { + originalTimes = originalTimes.filter((time) => time.date !== overrideDate); + + originalTimes.push( + ...timeRanges.map((rng) => ({ + date: overrideDate, + startMinutesSinceMidnight: rng.start.hour * 60 + rng.start.minute, + endMinutesSinceMidnight: rng.end.hour * 60 + rng.end.minute, + })) + ); + } + return originalTimes; +} diff --git a/src/db/query.ts b/src/db/query.ts deleted file mode 100644 index 0690c8e..0000000 --- a/src/db/query.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { emailTable, overwritesTable } from "./schema"; -import { ILocation } from "types"; -import { db } from "./db"; -import { notifySlack } from "utils/slack"; - -export async function getEmails(): Promise<{ name: string; email: string }[]> { - const result = await db - .select({ - name: emailTable.name, - email: emailTable.email, - }) - .from(emailTable); - - // Remove 'mailto:' if present - return result.map((row) => ({ - name: row.name, - email: row.email.replace(/^mailto:/, ""), - })); -} - -/** - * - * @returns object that maps ids to overrides, where each value in the override map is guaranteed to be non-null. (important!!) - */ -export async function getOverrides() { - const overrides = await db - .select() - .from(overwritesTable) - .catch((e) => { - notifySlack(` Failed to fetch overwrites with error ${e}`); - return []; - }); - - const idToOverrideMap = overrides.reduce<{ - [conceptId in string]?: Partial; - }>((accumulator, curLocation) => { - const reformattedObjWithNonNullFields: Partial = { - ...(curLocation.acceptsOnlineOrders !== null && { - acceptsOnlineOrders: curLocation.acceptsOnlineOrders, - }), - ...(curLocation.description !== null && { - description: curLocation.description, - }), - ...(curLocation.location !== null && { location: curLocation.location }), - ...(curLocation.menu !== null && { menu: curLocation.menu }), - ...(curLocation.name !== null && { name: curLocation.name }), - ...(curLocation.shortDescription !== null && { - shortDescription: curLocation.shortDescription, - }), - ...(curLocation.times !== null && { times: curLocation.times }), - ...(curLocation.url !== null && { url: curLocation.url }), - ...(curLocation.coordinateLat !== null && - curLocation.coordinateLng !== null && { - coordinates: { - lat: curLocation.coordinateLat, - lng: curLocation.coordinateLng, - }, - }), - }; - return { - ...accumulator, - [curLocation.conceptId]: { - ...Object.fromEntries( - Object.entries(reformattedObjWithNonNullFields).filter( - ([_, v]) => v !== null - ) - ), - }, - }; - }, {}); - return idToOverrideMap; -} diff --git a/src/db/schema.ts b/src/db/schema.ts index d53a385..357463b 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,32 +1,100 @@ +// don't add .unique() or .notNull() to a primary key, drizzle doesn't like it when you remove it later on. + import { pgTable, text, integer, boolean, - jsonb, decimal, + date, + primaryKey, + index, + pgEnum, } from "drizzle-orm/pg-core"; -import { ITimeRange } from "../types"; export const emailTable = pgTable("emails", { - id: integer().primaryKey().generatedAlwaysAsIdentity(), + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), name: text("name").notNull(), email: text("email").notNull(), }); - +export const conceptIdToInternalIdTable = pgTable("concept_id_to_internal_id", { + // keeping both as text for the most flexibility + internalId: text("internal_id") + .notNull() + .references(() => locationDataTable.id, { onDelete: "cascade" }), + externalId: text("external_id").unique().primaryKey(), +}); +export const locationDataTable = pgTable("location_data", { + id: text("id").primaryKey(), + name: text("name"), + shortDescription: text("short_description"), + description: text("description").notNull(), + url: text("url").notNull(), + menu: text("menu"), + /** The human-readable version of the location */ + location: text("location").notNull(), + coordinateLat: decimal("coordinate_lat", { mode: "number", scale: 30 }), + coordinateLng: decimal("coordinate_lng", { mode: "number", scale: 30 }), + acceptsOnlineOrders: boolean("accepts_online_orders").notNull(), +}); +export const timesTable = pgTable( + "location_times", + { + id: integer("id").notNull().generatedAlwaysAsIdentity().primaryKey(), + locationId: text("location_id") + .references(() => locationDataTable.id, { + onDelete: "cascade", + }) + .notNull(), + date: date("date").notNull(), + startTime: integer("start_time").notNull(), + endTime: integer("end_time").notNull(), + }, + (table) => [index("date_lookup").on(table.locationId, table.date)] +); /** * Includes everything in ILocation except for soups and specials */ export const overwritesTable = pgTable("overwrites_table", { - conceptId: integer("concept_id").notNull().primaryKey(), + locationId: text("location_id") + .primaryKey() + .references(() => locationDataTable.id, { + onDelete: "cascade", + }), name: text("name"), description: text("description"), shortDescription: text("short_description"), url: text("url"), menu: text("menu"), location: text("location"), - coordinateLat: decimal({ mode: "number", scale: 30 }), - coordinateLng: decimal({ mode: "number", scale: 30 }), + coordinateLat: decimal("coordinate_lat", { mode: "number", scale: 30 }), + coordinateLng: decimal("coordinate_lng", { mode: "number", scale: 30 }), acceptsOnlineOrders: boolean("accepts_online_orders"), - times: jsonb("times").$type(), +}); +export const timeOverwritesTable = pgTable( + "time_overwrites_table", + { + locationId: text("location_id") + .notNull() + .references(() => locationDataTable.id, { + onDelete: "cascade", + }), + date: date("date").notNull(), + timeString: text("time_string").notNull(), + }, + (table) => [primaryKey({ columns: [table.locationId, table.date] })] +); +export const specialType = pgEnum("specialType", ["special", "soup"]); + +export const specialsTable = pgTable("specials", { + id: integer("id").notNull().generatedAlwaysAsIdentity().primaryKey(), + locationId: text("location_id") + .references(() => locationDataTable.id, { + onDelete: "cascade", + }) + .notNull(), + name: text("name").notNull(), + description: text("description").notNull(), + date: date("date").notNull(), + type: specialType("type").notNull(), }); diff --git a/src/db/seed.ts b/src/db/seed.ts index 4f364af..7fa3b22 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,7 +1,7 @@ -import { db } from "./db"; +import { DBType } from "./db"; import { emailTable } from "./schema"; -export async function populateEmails() { +export async function populateEmails(db: DBType) { await db.insert(emailTable).values([ { email: "czech@un.org", diff --git a/src/db/updateLocation.ts b/src/db/updateLocation.ts new file mode 100644 index 0000000..78ed001 --- /dev/null +++ b/src/db/updateLocation.ts @@ -0,0 +1,147 @@ +import { ILocation } from "types"; +import { DBType } from "./db"; +import { + conceptIdToInternalIdTable, + locationDataTable, + specialsTable, + timeOverwritesTable, + timesTable, +} from "./schema"; +import { and, eq, gte } from "drizzle-orm"; +import { pad } from "utils/timeUtils"; +import { DateTime } from "luxon"; +async function getInternalId(db: DBType, externalId: string) { + let [idMapping] = await db + .select() + .from(conceptIdToInternalIdTable) + .where(eq(conceptIdToInternalIdTable.externalId, externalId)); + + return idMapping?.internalId ?? crypto.randomUUID(); +} + +/** + * + * @param db + * @param location + * @returns the internal id of the location that was added + */ +export async function addLocationDataToDb(db: DBType, location: ILocation) { + const internalId = await getInternalId(db, location.conceptId.toString()); + + const locationDbEntry: typeof locationDataTable.$inferInsert = { + id: internalId, + name: location.name, + shortDescription: location.shortDescription, + description: location.description, + url: location.url, + menu: location.menu, + location: location.location, + coordinateLat: location.coordinates?.lat, + coordinateLng: location.coordinates?.lng, + acceptsOnlineOrders: location.acceptsOnlineOrders, + }; + await db.transaction(async (tx) => { + await tx + .insert(locationDataTable) + .values(locationDbEntry) + .onConflictDoUpdate({ + target: locationDataTable.id, + set: locationDbEntry, + }); + + const todayAsSQLString = `${location.today.year}-${pad( + location.today.month + )}-${pad(location.today.day)}`; + // add specials + await tx + .delete(specialsTable) + .where( + and( + eq(specialsTable.locationId, internalId), + eq(specialsTable.date, todayAsSQLString) + ) + ); + const specials = [ + ...(location.todaysSpecials?.map((sp) => ({ + ...sp, + type: "special" as const, + })) ?? []), + ...(location.todaysSoups?.map((sp) => ({ + ...sp, + type: "soup" as const, + })) ?? []), + ]; + if (specials.length) + await tx.insert(specialsTable).values( + specials.map((special) => ({ + date: todayAsSQLString, + locationId: internalId, + name: special.title, + description: special.description, + type: special.type, + })) + ); + + // remove rows from whenever the scrape started from (aka remove entries corresponding to the last 7 days) + await tx + .delete(timesTable) + .where( + and( + eq(timesTable.locationId, internalId), + and(gte(timesTable.date, todayAsSQLString)) + ) + ); + if (location.times.length) { + await tx.insert(timesTable).values( + location.times.map((time) => ({ + locationId: internalId, + date: `${time.year}-${pad(time.month)}-${pad(time.day)}`, + startTime: time.startMinutesFromMidnight, + endTime: time.endMinutesFromMidnight, + })) + ); + } + + // in case the conceptId->internalId mapping entry isn't there + await tx + .insert(conceptIdToInternalIdTable) + .values({ + internalId: internalId, + externalId: location.conceptId.toString(), + }) + .onConflictDoNothing({ target: conceptIdToInternalIdTable.externalId }); + }); + return internalId; +} +/** + * + * @param db + * @param locationId + * @param date + * @param timeStringOverride + * @returns if successful + */ +export async function addTimeOverride( + db: DBType, + locationId: string, + date: string, + timeStringOverride: string +) { + const parsedDate = DateTime.fromFormat(date, "M/d/yy"); + if (!parsedDate.isValid) { + return false; + } + const rowToInsert: typeof timeOverwritesTable.$inferInsert = { + date: parsedDate.toSQLDate(), + locationId: locationId, + timeString: timeStringOverride, + }; + await db + .insert(timeOverwritesTable) + .values(rowToInsert) + .onConflictDoUpdate({ + target: [timeOverwritesTable.locationId, timeOverwritesTable.date], + set: rowToInsert, + }); + return true; +} diff --git a/src/deprecationNotice.ts b/src/deprecationNotice.ts new file mode 100644 index 0000000..ba1ae2b --- /dev/null +++ b/src/deprecationNotice.ts @@ -0,0 +1,45 @@ +export const deprecatedNotice: ILocationOld[] = [ + { + acceptsOnlineOrders: false, + conceptId: -1111, + description: "Please use the new /api/v2/locations endpoint", + name: "This api format has been deprecated", + location: "Now", + times: [], + url: "https://cmueats.com", + }, +]; + +interface ILocationOld { + conceptId: number; + name: string; + shortDescription?: string; + description: string; + url: string; + menu?: string; + location: string; + coordinates?: ICoordinate; + acceptsOnlineOrders: boolean; + times: ITimeRange[]; + todaysSpecials?: ISpecial[]; + todaysSoups?: ISpecial[]; +} +interface ISpecial { + title: string; + description: string; +} + +interface ITimeSlot { + day: number; + hour: number; + minute: number; +} + +interface ITimeRange { + start: ITimeSlot; + end: ITimeSlot; +} +interface ICoordinate { + lat: number; + lng: number; +} diff --git a/src/parser/diningParser.ts b/src/parser/diningParser.ts index b7cf56a..44c847d 100644 --- a/src/parser/diningParser.ts +++ b/src/parser/diningParser.ts @@ -3,6 +3,7 @@ import { load } from "cheerio"; import LocationBuilder from "../containers/locationBuilder"; import { retrieveSpecials } from "../containers/specials/specialsBuilder"; import { ILocation, ISpecial } from "types"; +import { notifySlack } from "utils/slack"; /** * Retrieves the HTML from the CMU Dining website and parses the information @@ -16,8 +17,6 @@ export default class DiningParser { static readonly DINING_SOUPS_URL = "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Soups"; - constructor() {} - async process(): Promise { const locationBuilders = await this.initializeLocationBuildersFromMainPage(); @@ -25,18 +24,28 @@ export default class DiningParser { const [specials, soups] = await this.fetchSpecials(); for (const builder of locationBuilders) { - await builder.populateDetailedInfo(); + await builder + .populateDetailedInfo() + .catch((e) => + notifySlack( + ` failed to parse page with id ${builder.getConceptId()} with error ${ + e.stack + }` + ) + ); builder.setSoup(soups); builder.setSpecials(specials); } - return locationBuilders.map((builder) => builder.build()); + return locationBuilders + .map((builder) => builder.build()) + .filter((location) => location !== undefined); // remove the unsuccessful parses } private async initializeLocationBuildersFromMainPage(): Promise< LocationBuilder[] > { - const mainPageHTML = await getHTMLResponse( + const { body: mainPageHTML } = await getHTMLResponse( new URL(DiningParser.DINING_URL) ); const mainContainer = load(mainPageHTML)("div.conceptCards"); @@ -53,10 +62,14 @@ export default class DiningParser { > { return await Promise.all([ retrieveSpecials( - await getHTMLResponse(new URL(DiningParser.DINING_SPECIALS_URL)) + ( + await getHTMLResponse(new URL(DiningParser.DINING_SPECIALS_URL)) + ).body ), retrieveSpecials( - await getHTMLResponse(new URL(DiningParser.DINING_SOUPS_URL)) + ( + await getHTMLResponse(new URL(DiningParser.DINING_SOUPS_URL)) + ).body ), ]); } diff --git a/src/server.ts b/src/server.ts index 4d7de5f..624110c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,19 +5,23 @@ import { ILocation } from "types"; import { env } from "env"; import { notifySlack } from "utils/slack"; import { node } from "@elysiajs/node"; +import ScrapeResultMerger from "utils/locationMerger"; +import { addLocationDataToDb } from "db/updateLocation"; +import { deprecatedNotice } from "deprecationNotice"; import { getDiffsBetweenLocationData } from "utils/diff"; -import LocationMerger from "utils/locationMerger"; -import { getEmails, getOverrides } from "db/query"; +import { getAllLocationsFromDB } from "db/getLocations"; +import { openapi } from "@elysiajs/openapi"; +import { initDBConnection } from "db/db"; +import { DateTime } from "luxon"; +import { QueryUtils } from "db/dbQueryUtils"; +/** only used for Slack debug diff logging */ let cachedLocations: ILocation[] = []; -function getCachedLocations() { - return applyOverrides(cachedLocations); -} async function reload(): Promise { const now = new Date(); console.log(`Reloading Dining API: ${now}`); const parser = new DiningParser(); - const locationMerger = new LocationMerger(); + const locationMerger = new ScrapeResultMerger(); for (let i = 0; i < env.NUMBER_OF_SCRAPES; i++) { // Wait a bit before starting the next round of scrapes. @@ -44,21 +48,14 @@ async function reload(): Promise { await notifySlack(diff); } } - } -} -async function applyOverrides(locations: ILocation[]): Promise { - const overrides = await getOverrides(); - return locations.map((location) => { - const overrideData = overrides[location.conceptId]; - if (overrideData === undefined) return location; - return { - ...location, - ...overrideData, - }; - }); + await Promise.all( + finalLocations.map((location) => addLocationDataToDb(db, location)) + ); + } } -export const app = new Elysia({ adapter: node() }); // I don't trust bun (as a runtime) enough (Eric Xu - 7/18/2025). This may change in the future, but bun is currently NOT a full drop-in replacement for node and is still rather unstable from personal experience +const [pool, db] = initDBConnection(env.DATABASE_URL); +export const app = new Elysia({ adapter: node() }).use(openapi()); // I don't trust bun (as a runtime) enough (Eric Xu - 7/18/2025). This may change in the future, but bun is currently NOT a full drop-in replacement for node and is still rather unstable from personal experience app.onError(({ error, path, code }) => { if (code === "NOT_FOUND") { @@ -72,15 +69,55 @@ app.onError(({ error, path, code }) => { } }); app.use(cors()); +app.onAfterHandle(({ response }) => { + if (typeof response === "object") { + // pretty print this + return new Response(JSON.stringify(response, null, 4), { + headers: { "Content-Type": "application/json; charset=utf-8" }, + }); // we can actually set proper content-type headers this way + } +}); app.get("/", () => { return "ScottyLabs Dining API"; }); +app.get( + "/api/v2/locations", + async () => await getAllLocationsFromDB(db, DateTime.now()) +); +app.get("/api/emails", async () => await new QueryUtils(db).getEmails()); + +app.post( + "/api/sendSlackMessage", + async ({ body: { message } }) => { + await notifySlack(message, env.SLACK_FRONTEND_WEBHOOK_URL); + }, + { + body: t.Object({ + message: t.String(), + }), + } +); + +setInterval(() => { + reload().catch( + (er) => `Error in reload process: ${notifySlack(String(er))}\n${er.stack}` + ); +}, env.RELOAD_WAIT_INTERVAL); +reload().catch( + (er) => `Error in reload process: ${notifySlack(String(er))}\n${er.stack}` +); + +app.listen(env.PORT, ({ hostname, port }) => { + notifySlack(`Dining API is running at ${hostname}:${port}`); +}); -app.get("/locations", async () => ({ locations: await getCachedLocations() })); +// DEPRECATED + +app.get("/locations", async () => ({ locations: deprecatedNotice })); app.get("/location/:name", async ({ params: { name } }) => { - const filteredLocation = (await getCachedLocations()).filter((location) => { + const filteredLocation = deprecatedNotice.filter((location) => { return location.name?.toLowerCase().includes(name.toLowerCase()); }); return { @@ -91,7 +128,7 @@ app.get("/location/:name", async ({ params: { name } }) => { app.get( "/locations/time/:day/:hour/:min", async ({ params: { day, hour, min } }) => { - const result = (await getCachedLocations()).filter((el) => { + const result = deprecatedNotice.filter((el) => { let returning = false; el.times.forEach((element) => { const startMins = @@ -111,37 +148,3 @@ app.get( return { locations: result }; } ); - -app.get("/api/emails", getEmails); -app.get("/api/changes", async () => await getOverrides()); - -app.post( - "/api/sendSlackMessage", - async ({ body: { message } }) => { - await notifySlack(message, env.SLACK_FRONTEND_WEBHOOK_URL); - }, - { - body: t.Object({ - message: t.String(), - }), - } -); - -setInterval(() => { - reload().catch( - (er) => `Error in reload process: ${notifySlack(String(er))}\n${er.stack}` - ); -}, env.RELOAD_WAIT_INTERVAL); - -// Initial load and start the server -reload() - .then(() => { - app.listen(env.PORT, ({ hostname, port }) => { - notifySlack(`Dining API is running at ${hostname}:${port}`); - }); - }) - .catch(async (er) => { - await notifySlack(" Dining API startup failed!!"); - await notifySlack(`*Error caught*\n${er.stack}`); - process.exit(1); - }); diff --git a/src/types.ts b/src/types.ts index 0a5cef5..03e48de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,31 +1,39 @@ +export interface IDate { + year: number; + /** 1-12 */ + month: number; + /** 1-31 */ + day: number; +} export interface ILocation { conceptId: number; name: string; - shortDescription?: string; + shortDescription: string | undefined; description: string; url: string; - menu?: string; + menu: string | undefined; location: string; - coordinates?: ICoordinate; + coordinates: ICoordinate | undefined; acceptsOnlineOrders: boolean; - times: ITimeRange[]; - todaysSpecials?: ISpecial[]; - todaysSoups?: ISpecial[]; + /** Assuming these times fall after today */ + times: IFullTimeRange[]; + /** used when figuring out which time entries to clear in the database. (we can't just look at `times` directly to figure that out, because it might as well be empty) (also, technically if this data is in the future, times won't be cleared properly. we hope that isn't the case though) */ + today: IDate; + todaysSpecials: ISpecial[] | undefined; + todaysSoups: ISpecial[] | undefined; } export interface ISpecial { title: string; description: string; } -export interface ITimeSlot { - day: DayOfTheWeek; - hour: number; - minute: number; -} - -export interface ITimeRange { - start: ITimeSlot; - end: ITimeSlot; +export interface IFullTimeRange { + year: number; + month: number; + day: number; + startMinutesFromMidnight: number; + /** Can be less than start if the time slot wraps around to the next day (eg. 2 PM - 2 AM) */ + endMinutesFromMidnight: number; } export interface ICoordinate { lat: number; @@ -36,16 +44,6 @@ export interface ILocationCoordinateOverwrites { [conceptId: string]: ICoordinate; } -export enum DayOfTheWeek { - SUNDAY = 0, - MONDAY = 1, - TUESDAY = 2, - WEDNESDAY = 3, - THURSDAY = 4, - FRIDAY = 5, - SATURDAY = 6, -} - export enum MonthOfTheYear { JANUARY = 1, FEBRUARY = 2, @@ -62,8 +60,6 @@ export enum MonthOfTheYear { } export enum TimeInfoType { - DAY = "DAY", - DATE = "DATE", TIME = "TIME", CLOSED = "CLOSED", TWENTYFOURHOURS = "TWENTYFOURHOURS", diff --git a/src/utils/diff.ts b/src/utils/diff.ts index 50f992e..ecbc60c 100644 --- a/src/utils/diff.ts +++ b/src/utils/diff.ts @@ -4,7 +4,7 @@ type T = { [key: string]: T } | T[] | string | number | undefined; export function getObjDiffs( prevObject: T, newObject: T, - path: string = "~" // ~uwu senpai + path: string = "~" ): string[] { let diffs: string[] = []; if (typeof prevObject === "object" && typeof newObject === "object") { @@ -88,7 +88,7 @@ function getArrayDiffs(prevArray: any[], newArray: any[], path: string) { if (prevFreqCnt[key] === undefined) { diffs.push( `inserted value at ${path} ${ - newFreqCnt[key] > 1 ? newFreqCnt[key] + " times" : "" + newFreqCnt[key]! > 1 ? newFreqCnt[key] + " times" : "" }: ${key}` ); } else if (newFreqCnt[key] === undefined) { diff --git a/src/utils/locationMerger.ts b/src/utils/locationMerger.ts index b450a4f..6befd2a 100644 --- a/src/utils/locationMerger.ts +++ b/src/utils/locationMerger.ts @@ -1,6 +1,6 @@ import { ILocation } from "types"; -export default class LocationMerger { +export default class ScrapeResultMerger { majorityDict: Partial< Record< number, diff --git a/src/utils/parseTimeToken.ts b/src/utils/parseTimeToken.ts index 8b03078..98eee63 100644 --- a/src/utils/parseTimeToken.ts +++ b/src/utils/parseTimeToken.ts @@ -1,28 +1,10 @@ import ParsedTime from "containers/time/parsedTime"; -import ParsedTimeForDate, { - convertMonthStringToEnum, -} from "containers/time/parsedTimeForDate"; -import ParsedTimeForDay, { - convertDayStringToEnum, -} from "containers/time/parsedTimeForDay"; +import { convertMonthStringToEnum } from "containers/time/parsedTimeForDate"; +import { convertDayStringToEnum } from "containers/time/parsedTimeForDay"; import { TimeInfoType } from "types"; export function parseToken(token: string) { token = token.trim().toLowerCase(); - if (isDay(token)) { - return { - type: TimeInfoType.DAY, - value: new ParsedTimeForDay(token).parse().value, - } as const; - } - - const testMonth = token.split(/\s/)[0]; - if (isMonth(testMonth)) { - return { - type: TimeInfoType.DATE, - value: new ParsedTimeForDate(token).parse().value, - } as const; - } if ( token === "24 hours" || token === "24 hrs" || diff --git a/src/utils/requestUtils.ts b/src/utils/requestUtils.ts index c197e6d..2d44fd5 100644 --- a/src/utils/requestUtils.ts +++ b/src/utils/requestUtils.ts @@ -1,15 +1,17 @@ import axios from "axios"; import { notifySlack } from "./slack"; import { env } from "env"; - +import { DateTime } from "luxon"; const wait = (ms: number) => { return new Promise((re) => setTimeout(re, ms)); }; - -export async function getHTMLResponse( - url: URL, - retriesLeft = 4 -): Promise { +/** + * + * @param url + * @param retriesLeft + * @returns the serverDate returned is in EST time, since that's the timezone that the dining server operates in (ex. at midnight est, the 7-day times shift to the next day.) + */ +export async function getHTMLResponse(url: URL, retriesLeft = 4) { try { if (!env.IN_TEST_MODE) console.log(`Scraping ${url}`); const response = await axios.get(url.toString()); @@ -20,7 +22,20 @@ export async function getHTMLResponse( url: url.toString(), }); - return response.data; + const attemptedParsedDate = DateTime.fromRFC2822( + response.headers.date + ).setZone("America/New_York"); + if (!attemptedParsedDate.isValid) { + notifySlack( + ` Ran into unparseable date response header! url: ${url} response headers: ${response.headers.date}` + ); + } + return { + body: response.data, + serverDate: attemptedParsedDate.isValid + ? attemptedParsedDate + : (DateTime.now().setZone("America/New_York") as DateTime), // date should always be valid... + }; } catch (err: any) { notifySlack(`Error scraping ${url}\n${err.stack}`); if (retriesLeft > 0) { diff --git a/src/utils/slack.ts b/src/utils/slack.ts index 16d5720..c6e49ca 100644 --- a/src/utils/slack.ts +++ b/src/utils/slack.ts @@ -3,6 +3,7 @@ import { env } from "env"; async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + /** * * @param message @@ -13,7 +14,10 @@ export async function notifySlack( message: string, slackUrl: string = env.SLACK_BACKEND_WEBHOOK_URL ) { - if (env.IN_TEST_MODE) return; + if (env.IN_TEST_MODE) { + console.log("would've notified slack with message", message); + return; + } console.log("Sending message to slack:", message); try { await axios.post( diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts index beccb11..240e694 100644 --- a/src/utils/timeUtils.ts +++ b/src/utils/timeUtils.ts @@ -1,78 +1,47 @@ -import { DayOfTheWeek, ITimeSlot, ITimeRange } from "types"; +import { ITimeRangeInternal } from "db/dbQueryUtils"; +import { DateTime } from "luxon"; -export function getNextDay(day: DayOfTheWeek): DayOfTheWeek { - const weekdays: DayOfTheWeek[] = [ - DayOfTheWeek.SUNDAY, - DayOfTheWeek.MONDAY, - DayOfTheWeek.TUESDAY, - DayOfTheWeek.WEDNESDAY, - DayOfTheWeek.THURSDAY, - DayOfTheWeek.FRIDAY, - DayOfTheWeek.SATURDAY, - ]; // ordered by time - return weekdays[(weekdays.indexOf(day) + 1) % 7]; -} - -export function getMinutesSinceStartOfSunday(timeSlot: ITimeSlot) { - return timeSlot.day * (24 * 60) + timeSlot.hour * 60 + timeSlot.minute; -} /** * - * @param timeSlot1 - * @param timeSlot2 - * @returns Delta in minutes of moment1 - moment2 + * @param timeRanges does not need to be sorted + * @returns */ -export function compareTimeSlots(timeSlot1: ITimeSlot, timeSlot2: ITimeSlot) { - return ( - getMinutesSinceStartOfSunday(timeSlot1) - - getMinutesSinceStartOfSunday(timeSlot2) - ); -} - -export function sortAndMergeTimeRanges(timeRanges: ITimeRange[]) { - const MINUTES_IN_A_WEEK = 60 * 24 * 7; - const unwrappedTimeRanges = timeRanges - .flatMap((rng) => { - if (compareTimeSlots(rng.start, rng.end) > 0) { - // unwrap the wrapped interval - return [ - { start: { day: 0, hour: 0, minute: 0 }, end: rng.end }, - { start: rng.start, end: { day: 6, hour: 23, minute: 59 } }, - ]; - } else { - return [rng]; - } - }) - .sort((range1, range2) => compareTimeSlots(range1.start, range2.start)); - const mergedRanges: ITimeRange[] = []; - - for (const timeRange of unwrappedTimeRanges) { - const lastTimeRange = mergedRanges.length +export function mergeTimeRanges( + timeRanges: { start: number; end: number }[], + slackBetweenRanges = 0 +) { + const mergedRanges: { start: number; end: number }[] = []; + timeRanges.sort((a, b) => a.start - b.start); + for (const rng of timeRanges) { + const lastMergedRange = mergedRanges.length ? mergedRanges[mergedRanges.length - 1] : undefined; if ( - lastTimeRange && - compareTimeSlots(lastTimeRange.end, timeRange.start) >= -1 // we overlap 1-minute disjoint intervals as well (ex. 2:00 PM - 2:59 PM will get merged with 3:00PM - 4:00PM as 2:00PM - 4:00PM) + lastMergedRange && + lastMergedRange.end + slackBetweenRanges >= rng.start ) { - if (compareTimeSlots(timeRange.end, lastTimeRange.end) > 0) { - lastTimeRange.end = timeRange.end; // join current range with last range - } + lastMergedRange.end = Math.max(lastMergedRange.end, rng.end); } else { - mergedRanges.push(timeRange); + mergedRanges.push(rng); } } - // merge the last day with the first day if needed - if (mergedRanges.length >= 2) { - const lastRange = mergedRanges[mergedRanges.length - 1]; - const firstRange = mergedRanges[0]; - if ( - getMinutesSinceStartOfSunday(lastRange.end) === MINUTES_IN_A_WEEK - 1 && - getMinutesSinceStartOfSunday(firstRange.start) === 0 - ) { - lastRange.end = firstRange.end; - mergedRanges.shift(); - } - } - return mergedRanges; } +export function pad(n: number) { + return n.toString().padStart(2, "0"); +} +/** Wraps time intervals to the next day if end < start, and then merges everything */ +export function remapAndMergeTimeIntervals(timeRanges: ITimeRangeInternal[]) { + const timeStampedIntervals = timeRanges.map((rng) => { + const date = DateTime.fromISO(rng.date, { zone: "America/New_York" }); + const startDate = date.set({ minute: rng.startMinutesSinceMidnight }); + const endDate = date.set({ minute: rng.endMinutesSinceMidnight }).plus({ + days: rng.endMinutesSinceMidnight < rng.startMinutesSinceMidnight ? 1 : 0, + }); // account for wrap-around + return { + start: startDate.toMillis(), + end: endDate.toMillis(), + }; + }); + return mergeTimeRanges(timeStampedIntervals, 60 * 1000); // merge gaps of at most 1 min +} diff --git a/tests/database.test.ts b/tests/database.test.ts new file mode 100644 index 0000000..4c17ce3 --- /dev/null +++ b/tests/database.test.ts @@ -0,0 +1,708 @@ +import { + PostgreSqlContainer, + StartedPostgreSqlContainer, +} from "@testcontainers/postgresql"; +import { DBType, initDBConnection } from "db/db"; +import { addLocationDataToDb, addTimeOverride } from "db/updateLocation"; +import { getAllLocationsFromDB } from "db/getLocations"; +import { DateTime } from "luxon"; +import { ILocation } from "types"; +import { test as baseTest } from "vitest"; +import { Pool } from "pg"; +import { overwritesTable } from "db/schema"; + +/** Something that you can get from DiningParser */ +const locationIn: ILocation = { + name: "dbTest", + acceptsOnlineOrders: false, + conceptId: 1, + coordinates: { lat: 1, lng: 10 }, + description: "description", + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [], + location: "location", + menu: "menu", + shortDescription: "hi", + url: "https://hi.com", + todaysSoups: [], + todaysSpecials: [], +}; + +/** What getAllLocations() returns when `locationIn` has been added to the db */ +const locationOut = { + id: "DYNAMICALLY GENERATED, replace with real id", + name: "dbTest", + shortDescription: "hi", + description: "description", + url: "https://hi.com", + menu: "menu", + location: "location", + coordinateLat: 1, + coordinateLng: 10, + acceptsOnlineOrders: false, + times: [], + todaysSoups: [], + todaysSpecials: [], +}; +const dbTest = baseTest.extend<{ + ctx: { + db: DBType; + container: StartedPostgreSqlContainer; + pool: Pool; + }; +}>({ + ctx: async ({}, use) => { + const container = await new PostgreSqlContainer("postgres:17.5") + .withCopyDirectoriesToContainer([ + { + source: `${__dirname}/../drizzle`, + target: "/docker-entrypoint-initdb.d", + }, + ]) + .start(); + const [pool, db] = initDBConnection(container.getConnectionUri()); + use({ container, pool, db }); + }, +}); + +dbTest.afterEach(({ ctx }) => { + ctx.pool.end(); + ctx.container.stop(); +}); + +describe("DB", () => { + dbTest.concurrent("works on basic insertion", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, locationIn); + const dbResult = await getAllLocationsFromDB(db, DateTime.now()); + expect(dbResult).toEqual([{ ...locationOut, id }]); + }); + dbTest.concurrent( + "properly resets state on every new dbTest", + async ({ ctx: { db } }) => { + expect(await getAllLocationsFromDB(db, DateTime.now())).toEqual([]); + } + ); + dbTest.concurrent( + "works on insertion with times", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "2:00 pm", "2:00 am"), + parseTime("7/7/25", "2:00 pm", "2:00 am"), + ], + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/2/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("1/1/25 5:00 AM"), + end: timeToUnixTimestamp("1/1/25 12:00 PM"), + }, + { + start: timeToUnixTimestamp("1/1/25 2:00 PM"), + end: timeToUnixTimestamp("1/2/25 2:00 AM"), + }, + { + start: timeToUnixTimestamp("7/7/25 2:00 PM"), + end: timeToUnixTimestamp("7/8/25 2:00 AM"), + }, + ], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (tests search window)", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "2:00 pm", "2:00 am"), + ], + }); + const dbResult = await getAllLocationsFromDB( + db, + parseDate("1/3/25") // 2 days after latest time + ); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + + times: [], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (tests search window)", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("2/1/25", "5:00 am", "12:00 pm"), + ], + }); + const dbResult = await getAllLocationsFromDB( + db, + parseDate("1/3/25") // 2 days after latest time + ); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("2/1/25 5:00 AM"), + end: timeToUnixTimestamp("2/1/25 12:00 PM"), + }, + ], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (DST - start 2AM -> 3AM) (3/9/25)", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + times: [parseTime("3/9/25", "5:00 am", "12:00 pm")], + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/3/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("3/9/25 5:00 AM"), + end: timeToUnixTimestamp("3/9/25 12:00 PM"), + }, + ], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (DST - start 2AM -> 3AM) (3/9/25)", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + times: [parseTime("3/9/25", "2:30 am", "12:00 pm")], // 2:30 technically doesn't exist on that day + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/3/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("3/9/25 3:30 AM"), // 3:30 AM... this is certainly some behavior... + end: timeToUnixTimestamp("3/9/25 12:00 PM"), + }, + ], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (DST - end 2AM -> 1AM) (11/2/25) [if a place closes at 2 AM, we assume it's the second 2AM (I mean, the first 2AM technically doesn't exist...)", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [parseTime("11/1/25", "7:00 am", "2:00 am")], + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/3/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("11/1/25 7:00 AM"), + end: timeToUnixTimestamp("11/2/25 2:00 AM"), // the second 2AM + }, + ], + }, + ]); + } + ); + dbTest.concurrent( + "works on insertion with times (DST - end 2AM -> 1AM) (11/2/25) [if a place closes at 1:30 AM, we assume dbTest's the first 1:30 AM and not the second]", + async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [parseTime("11/1/25", "7:00 am", "1:30 am")], + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/3/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("11/1/25 7:00 AM"), + end: 1762061400000, // the first 1:30 AM EDT (timeToUnixTimestamp would give the second 1:30 AM EST due to DST fallback) + }, + ], + }, + ]); + } + ); + dbTest.concurrent("works on specials", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "2:00 pm", "2:00am"), + ], + todaysSoups: [ + { title: "soup 1", description: "desc 1" }, + { title: "soup 2", description: "desc 2" }, + ], + todaysSpecials: [ + { title: "special 1", description: "desc 1" }, + { title: "special 2", description: "desc 2" }, + ], + }); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/1/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id, + + times: [ + { + start: timeToUnixTimestamp("1/1/25 5:00 AM"), + end: timeToUnixTimestamp("1/1/25 12:00 PM"), + }, + { + start: timeToUnixTimestamp("1/1/25 2:00 PM"), + end: timeToUnixTimestamp("1/2/25 2:00 AM"), + }, + ], + todaysSoups: [ + { + description: "desc 1", + name: "soup 1", + }, + { + description: "desc 2", + name: "soup 2", + }, + ], + todaysSpecials: [ + { + description: "desc 1", + name: "special 1", + }, + { + description: "desc 2", + name: "special 2", + }, + ], + }, + ]); + }); + dbTest.concurrent( + "works on specials (overriding)", + async ({ ctx: { db } }) => { + const id1 = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "2:00 pm", "2:00 am"), + ], + todaysSoups: [ + { title: "soup 1", description: "desc 1" }, + { title: "soup 2", description: "desc 2" }, + ], + todaysSpecials: [ + { title: "special 1", description: "desc 1" }, + { title: "special 2", description: "desc 2" }, + ], + }); + const id2 = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "5:00 am", "12:00 pm"), + parseTime("1/1/25", "2:00 pm", "2:00 am"), + ], + todaysSoups: [], + todaysSpecials: [ + { title: "special 1", description: "desc 1" }, + { title: "special 2", description: "desc 2" }, + ], + }); + expect(id1).toEqual(id2); + const dbResult = await getAllLocationsFromDB(db, parseDate("1/1/25")); + expect(dbResult).toEqual([ + { + ...locationOut, + id: id1, + times: [ + { + start: timeToUnixTimestamp("1/1/25 5:00 AM"), + end: timeToUnixTimestamp("1/1/25 12:00 PM"), + }, + { + start: timeToUnixTimestamp("1/1/25 2:00 PM"), + end: timeToUnixTimestamp("1/2/25 2:00 AM"), + }, + ], + todaysSoups: [], + todaysSpecials: [ + { + description: "desc 1", + name: "special 1", + }, + { + description: "desc 2", + name: "special 2", + }, + ], + }, + ]); + } + ); + dbTest.concurrent("works on time overwrites", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + times: [ + parseTime("1/1/25", "5:00 am", "5:00 pm"), + parseTime("1/2/25", "5:00 am", "5:00 pm"), + parseTime("1/3/25", "5:00 am", "5:00 pm"), + parseTime("1/4/25", "5:00 am", "5:00 pm"), + parseTime("1/5/25", "5:00 am", "5:00 pm"), + ], + }); + const success1 = await addTimeOverride( + db, + id, + "1/1/25", + "2:00 AM - 3:00 PM" + ); + const success2 = await addTimeOverride( + db, + id, + "1/1/25", + "3:00 AM - 4:00 PM" + ); // second one should overwrite the first one + expect(success1).toBe(true); + expect(success2).toBe(true); + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("1/1/25 3:00 AM"), + end: timeToUnixTimestamp("1/1/25 4:00 PM"), + }, + { + start: timeToUnixTimestamp("1/2/25 5:00 AM"), + end: timeToUnixTimestamp("1/2/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/3/25 5:00 AM"), + end: timeToUnixTimestamp("1/3/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/4/25 5:00 AM"), + end: timeToUnixTimestamp("1/4/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/5/25 5:00 AM"), + end: timeToUnixTimestamp("1/5/25 5:00 PM"), + }, + ], + }, + ]); + }); + dbTest.concurrent("malformed time overwrites", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + times: [ + parseTime("1/1/25", "5:00 am", "5:00 pm"), + parseTime("1/2/25", "5:00 am", "5:00 pm"), + parseTime("1/3/25", "5:00 am", "5:00 pm"), + parseTime("1/4/25", "5:00 am", "5:00 pm"), + parseTime("1/5/25", "5:00 am", "5:00 pm"), + ], + }); + + const success = await addTimeOverride(db, id, "1/1/25", "moo"); + expect(success).toBe(true); + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + id: id, + times: [ + // 1/1/25 should be wiped because there is an overwrite -- it's just invalid + { + start: timeToUnixTimestamp("1/2/25 5:00 AM"), + end: timeToUnixTimestamp("1/2/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/3/25 5:00 AM"), + end: timeToUnixTimestamp("1/3/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/4/25 5:00 AM"), + end: timeToUnixTimestamp("1/4/25 5:00 PM"), + }, + { + start: timeToUnixTimestamp("1/5/25 5:00 AM"), + end: timeToUnixTimestamp("1/5/25 5:00 PM"), + }, + ], + }, + ]); + }); + dbTest.concurrent("works on general overwrites", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + acceptsOnlineOrders: false, + menu: "bleh", + }); + await db.insert(overwritesTable).values({ + locationId: id, + acceptsOnlineOrders: true, + menu: "overwritten menu", + }); + + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + id: id, + acceptsOnlineOrders: true, + menu: "overwritten menu", + }, + ]); + }); + dbTest.concurrent("two locations", async ({ ctx: { db } }) => { + const id1 = await addLocationDataToDb(db, { + ...locationIn, + conceptId: 1, + }); + const id2 = await addLocationDataToDb(db, { + ...locationIn, + conceptId: 2, + }); + expect(id1).not.toEqual(id2); + const locationData = await getAllLocationsFromDB(db, parseDate("1/1/25")); + expect(locationData).toHaveLength(2); + expect(locationData).toEqual( + expect.arrayContaining([ + { + ...locationOut, + id: id1, + }, + { + ...locationOut, + id: id2, + }, + ]) + ); + }); + dbTest.concurrent( + "adding times for the next day", + async ({ ctx: { db } }) => { + const id1 = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "7:00 AM", "2:00 PM"), + parseTime("1/2/25", "7:00 AM", "2:00 PM"), + parseTime("1/3/25", "7:00 AM", "2:00 PM"), + ], + }); + const id2 = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 2, + }, + times: [parseTime("1/3/25", "7:00 AM", "7:00 PM")], + }); + expect(id1).toEqual(id2); + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + id: id1, + times: [ + { + start: timeToUnixTimestamp("1/1/25 7:00 AM"), + end: timeToUnixTimestamp("1/1/25 2:00 PM"), + }, + { + start: timeToUnixTimestamp("1/3/25 7:00 AM"), + end: timeToUnixTimestamp("1/3/25 7:00 PM"), + }, + ], + }, + ]); + } + ); + dbTest.concurrent("time merging", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + today: { + year: 2025, + month: 1, + day: 1, + }, + times: [ + parseTime("1/1/25", "7:00 AM", "2:00 PM"), + parseTime("1/1/25", "2:00 AM", "12:00 PM"), + parseTime("1/1/25", "2:01 PM", "9:00 PM"), + parseTime("1/1/25", "9:00 PM", "11:59 PM"), + parseTime("1/2/25", "12:00 AM", "11:59 PM"), + ], + }); + + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + id: id, + times: [ + { + start: timeToUnixTimestamp("1/1/25 2:00 AM"), + end: timeToUnixTimestamp("1/2/25 11:59 PM"), + }, + ], + }, + ]); + }); + dbTest.concurrent.skip("stub", async ({ ctx: { db } }) => { + const id = await addLocationDataToDb(db, { + ...locationIn, + }); + + expect(await getAllLocationsFromDB(db, parseDate("1/1/25"))).toEqual([ + { + ...locationOut, + }, + ]); + }); // just for reference +}); + +/** + * + * @param date in mm/dd/yy format + */ +function parseDate(date: string) { + const dateParsed = DateTime.fromFormat(date, "M/d/yy"); + if (!dateParsed.isValid) throw new Error(`Invalid date string ${date}`); + return dateParsed; +} +/** + * + * @param time ex. 2:00 AM (2:00AM is also fine) + * @returns minutes since midnight for that time + */ +function _parseTime(time: string) { + const [_, hour, minute, ampm] = [ + ...(/(\d*)\s*:\s*(\d*)\s*(AM|PM)/i.exec(time) ?? []), + ]; + if (hour === undefined || minute === undefined || ampm === undefined) + throw new Error(`Malformed time ${time}`); + const hourNum = parseInt(hour); + const minuteNum = parseInt(minute); + if (isNaN(hourNum) || isNaN(minuteNum)) + throw new Error(`Malformed time ${time}`); + const minutesSinceMidnight = + (hourNum % 12) * 60 + + minuteNum + + (ampm.toLowerCase() === "pm" ? 12 * 60 : 0); + return minutesSinceMidnight; +} +/** + * + * @param date ex. 2/3/25 + * @param startTime ex. 2:00 AM + * @param endTime ex. 2:00 AM (can wrap to next day) + */ +function parseTime(date: string, startTime: string, endTime: string) { + const parsedDate = parseDate(date); + return { + year: parsedDate.year, + month: parsedDate.month, + day: parsedDate.day, + startMinutesFromMidnight: _parseTime(startTime), + endMinutesFromMidnight: _parseTime(endTime), + }; +} + +/** + * + * @param datetime + * @returns timestamp, when datetime is interpreted in EST + */ +function timeToUnixTimestamp(datetime: string) { + const parsedDate = DateTime.fromFormat(datetime, "M/d/yy h:mm a", { + zone: "America/New_York", // important! enforces timezone + }); + if (!parsedDate.isValid) throw new Error(`Malformed date string ${datetime}`); + return parsedDate.toMillis(); +} + +const wait = (ms: number) => new Promise((re) => setTimeout(re, ms)); diff --git a/tests/diffLog.test.ts b/tests/diffLog.test.ts index ede0052..a65a167 100644 --- a/tests/diffLog.test.ts +++ b/tests/diffLog.test.ts @@ -1,7 +1,7 @@ import { getObjDiffs } from "../src/utils/diff"; -jest.mock("../src/utils/slack", () => { - return { notifySlack: jest.fn((str: string) => console.log(str)) }; +vi.mock("../src/utils/slack", () => { + return { notifySlack: vi.fn((str: string) => console.log(str)) }; }); describe("test diff checking", () => { diff --git a/tests/expectedData.ts b/tests/expectedData.ts index 5a9d02f..ae333a3 100644 --- a/tests/expectedData.ts +++ b/tests/expectedData.ts @@ -1,4 +1,6 @@ -export const expectedLocationData = [ +import { ILocation } from "types"; + +export const expectedLocationData: ILocation[] = [ { conceptId: 113, name: "AU BON PAIN AT SKIBO CAFÉ", @@ -9,38 +11,67 @@ export const expectedLocationData = [ url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/113", location: "Cohon Center, Second floor", menu: "https://web.archive.org/web/20230806004812/https://apps.studentaffairs.cmu.edu/dining/dashboard_images/Production/menus/113/abp-menu6.pdf", - coordinates: { lat: 40.44, lng: -79.94 }, + coordinates: { + lat: 40.44, + lng: -79.94, + }, acceptsOnlineOrders: true, times: [ { - start: { day: 0, hour: 16, minute: 30 }, - end: { day: 0, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 5, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1200, }, { - start: { day: 1, hour: 8, minute: 0 }, - end: { day: 1, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 6, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1200, }, { - start: { day: 2, hour: 8, minute: 0 }, - end: { day: 2, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 7, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 1200, }, { - start: { day: 3, hour: 8, minute: 0 }, - end: { day: 3, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 8, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 1200, }, { - start: { day: 4, hour: 8, minute: 0 }, - end: { day: 4, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 9, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 1200, }, { - start: { day: 5, hour: 8, minute: 0 }, - end: { day: 5, hour: 23, minute: 59 }, + year: 2024, + month: 8, + day: 10, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 1200, }, { - start: { day: 6, hour: 16, minute: 30 }, - end: { day: 6, hour: 20, minute: 0 }, + year: 2024, + month: 8, + day: 11, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 1439, }, ], + today: { + year: 2024, + month: 8, + day: 5, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -48,13 +79,22 @@ export const expectedLocationData = [ conceptId: 184, name: "CIAO BELLA", shortDescription: "Customizable pasta plates", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/184", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -63,13 +103,22 @@ export const expectedLocationData = [ name: "DE FER COFFEE & TEA AT MAGGIE MURPH CAFÉ", shortDescription: "Locally-roasted specialty coffee, tea, and scratch-made food", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/95", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -77,13 +126,22 @@ export const expectedLocationData = [ conceptId: 134, name: "E.A.T. (EVENINGS AT TEPPER) - ROHR COMMONS", shortDescription: "Dinner options at Tepper are Grubhub only!", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/134", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -91,13 +149,22 @@ export const expectedLocationData = [ conceptId: 178, name: "THE EDGE CAFE & MARKET", shortDescription: "Vaad-certified kosher bagels, pizza, bourekas & more!", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/178", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -106,13 +173,22 @@ export const expectedLocationData = [ name: "EGG SHOPPE - GRUBHUB ONLY", shortDescription: "Breakfast available only on Grubhub for pickup in Schatz.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/88", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -121,13 +197,22 @@ export const expectedLocationData = [ name: "ENTROPY+", shortDescription: "On-campus convenience store, serving snacks, grab-and-go meals, coffee, & more", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/103", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -141,30 +226,53 @@ export const expectedLocationData = [ url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/92", location: "Posner Hall, 1st Floor", menu: "https://web.archive.org/web/20240721001349/https://apps.studentaffairs.cmu.edu/dining/dashboard_images/Production/menus/92/menu-exchange-2024-25-v2.pdf", - coordinates: { lat: 40.441354, lng: -79.942125 }, + coordinates: { + lat: 40.441354, + lng: -79.942125, + }, acceptsOnlineOrders: false, times: [ { - start: { day: 1, hour: 8, minute: 0 }, - end: { day: 1, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 22, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 2, hour: 8, minute: 0 }, - end: { day: 2, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 23, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 3, hour: 8, minute: 0 }, - end: { day: 3, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 24, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 4, hour: 8, minute: 0 }, - end: { day: 4, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 25, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 5, hour: 8, minute: 0 }, - end: { day: 5, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 26, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, ], + today: { + year: 2024, + month: 7, + day: 20, + }, todaysSpecials: [ { title: "Brunch at The EXCHANGE Every Saturday", @@ -187,13 +295,22 @@ export const expectedLocationData = [ conceptId: 126, name: "FOOD HALL AT RESNIK", shortDescription: "Summer all-you-care-to-eat: OPENS JUNE 24!", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/126", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -202,13 +319,22 @@ export const expectedLocationData = [ name: "FORBES AVENUE SUBS - ROHR COMMONS", shortDescription: "Made-to-order deli-style subs and wraps. Vegan options available.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/173", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -217,13 +343,22 @@ export const expectedLocationData = [ name: "EL GALLO DE ORO", shortDescription: "Mexican cuisine, burritos and burrito bowls, tacos, quesadillas, salads", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/91", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -232,13 +367,22 @@ export const expectedLocationData = [ name: "GRANO PIZZA", shortDescription: "Hand-stretched, personal-sized pizzas on a New York or focaccia-style crust", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/139", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -251,34 +395,60 @@ export const expectedLocationData = [ url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/110", location: "Newell-Simon Atrium", menu: "https://web.archive.org/web/20240901003526/https://apps.studentaffairs.cmu.edu/dining/dashboard_images/Production/menus/110/Fall Menus 2024 (25).pdf", - coordinates: { lat: 40.4433922, lng: -79.9455957 }, + coordinates: { + lat: 40.4433922, + lng: -79.9455957, + }, acceptsOnlineOrders: true, times: [ { - start: { day: 0, hour: 12, minute: 0 }, - end: { day: 0, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 1, + startMinutesFromMidnight: 720, + endMinutesFromMidnight: 1200, }, { - start: { day: 1, hour: 10, minute: 30 }, - end: { day: 1, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 2, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 1200, }, { - start: { day: 2, hour: 10, minute: 30 }, - end: { day: 2, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 3, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 1200, }, { - start: { day: 3, hour: 10, minute: 30 }, - end: { day: 3, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 4, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 1200, }, { - start: { day: 4, hour: 10, minute: 30 }, - end: { day: 4, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 5, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 1200, }, { - start: { day: 5, hour: 10, minute: 30 }, - end: { day: 5, hour: 20, minute: 0 }, + year: 2024, + month: 9, + day: 6, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 1200, }, ], + today: { + year: 2024, + month: 8, + day: 31, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -287,13 +457,22 @@ export const expectedLocationData = [ name: "ROHR CAFÉ - LA PRIMA", shortDescription: "La Prima's second location on campus serving Italian-style coffee and food", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/115", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -302,13 +481,22 @@ export const expectedLocationData = [ name: "MILLIE'S COFFEE 'N' CREAMERY - ROHR COMMONS", shortDescription: "NOW OPEN! Sustainably sourced coffee, ice cream, and vegan gelato.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/136", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -317,13 +505,22 @@ export const expectedLocationData = [ name: "NOURISH", shortDescription: "Closed until fall semester: Grab and go available in Entropy", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/127", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -332,13 +529,22 @@ export const expectedLocationData = [ name: "LA PRIMA ESPRESSO", shortDescription: "Italian-style coffee, pastries, grab-and-go sandwiches, salads, and sides", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/94", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -347,13 +553,22 @@ export const expectedLocationData = [ name: "REDHAWK COFFEE", shortDescription: "Local coffee roaster serving specialty coffee, tea, baked goods, and grab-and-go food.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/186", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -362,13 +577,22 @@ export const expectedLocationData = [ name: "REVOLUTION NOODLE", shortDescription: "Customizable Malatang Noodle bowls, from the owners of Hunan Express", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/174", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -381,238 +605,151 @@ export const expectedLocationData = [ url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/108", location: "Cohon Center, Second floor", menu: undefined, - coordinates: { lat: 40.4429602, lng: -79.9418151 }, + coordinates: { + lat: 40.4429602, + lng: -79.9418151, + }, acceptsOnlineOrders: false, times: [ { - start: { - day: 0, - hour: 10, - minute: 30, - }, - end: { - day: 0, - hour: 14, - minute: 30, - }, - }, - { - start: { - day: 0, - hour: 16, - minute: 30, - }, - end: { - day: 0, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 1, - hour: 7, - minute: 30, - }, - end: { - day: 1, - hour: 10, - minute: 0, - }, - }, - { - start: { - day: 1, - hour: 11, - minute: 0, - }, - end: { - day: 1, - hour: 14, - minute: 0, - }, - }, - { - start: { - day: 1, - hour: 16, - minute: 30, - }, - end: { - day: 1, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 2, - hour: 7, - minute: 30, - }, - end: { - day: 2, - hour: 10, - minute: 0, - }, - }, - { - start: { - day: 2, - hour: 11, - minute: 0, - }, - end: { - day: 2, - hour: 14, - minute: 0, - }, - }, - { - start: { - day: 2, - hour: 16, - minute: 30, - }, - end: { - day: 2, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 3, - hour: 7, - minute: 30, - }, - end: { - day: 3, - hour: 10, - minute: 0, - }, - }, - { - start: { - day: 3, - hour: 11, - minute: 0, - }, - end: { - day: 3, - hour: 14, - minute: 0, - }, - }, - { - start: { - day: 3, - hour: 16, - minute: 30, - }, - end: { - day: 3, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 4, - hour: 7, - minute: 30, - }, - end: { - day: 4, - hour: 10, - minute: 0, - }, - }, - { - start: { - day: 4, - hour: 11, - minute: 0, - }, - end: { - day: 4, - hour: 14, - minute: 0, - }, - }, - { - start: { - day: 4, - hour: 16, - minute: 30, - }, - end: { - day: 4, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 5, - hour: 7, - minute: 30, - }, - end: { - day: 5, - hour: 10, - minute: 0, - }, - }, - { - start: { - day: 5, - hour: 11, - minute: 0, - }, - end: { - day: 5, - hour: 14, - minute: 0, - }, - }, - { - start: { - day: 5, - hour: 16, - minute: 30, - }, - end: { - day: 5, - hour: 20, - minute: 30, - }, - }, - { - start: { - day: 6, - hour: 10, - minute: 30, - }, - end: { - day: 6, - hour: 14, - minute: 30, - }, - }, - { - start: { - day: 6, - hour: 16, - minute: 30, - }, - end: { - day: 6, - hour: 20, - minute: 30, - }, + year: 2024, + month: 9, + day: 20, + startMinutesFromMidnight: 450, + endMinutesFromMidnight: 600, + }, + { + year: 2024, + month: 9, + day: 20, + startMinutesFromMidnight: 660, + endMinutesFromMidnight: 840, + }, + { + year: 2024, + month: 9, + day: 20, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 21, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 870, + }, + { + year: 2024, + month: 9, + day: 21, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 22, + startMinutesFromMidnight: 630, + endMinutesFromMidnight: 870, + }, + { + year: 2024, + month: 9, + day: 22, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 23, + startMinutesFromMidnight: 450, + endMinutesFromMidnight: 600, + }, + { + year: 2024, + month: 9, + day: 23, + startMinutesFromMidnight: 660, + endMinutesFromMidnight: 840, + }, + { + year: 2024, + month: 9, + day: 23, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 24, + startMinutesFromMidnight: 450, + endMinutesFromMidnight: 600, + }, + { + year: 2024, + month: 9, + day: 24, + startMinutesFromMidnight: 660, + endMinutesFromMidnight: 840, + }, + { + year: 2024, + month: 9, + day: 24, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 25, + startMinutesFromMidnight: 450, + endMinutesFromMidnight: 600, + }, + { + year: 2024, + month: 9, + day: 25, + startMinutesFromMidnight: 660, + endMinutesFromMidnight: 840, + }, + { + year: 2024, + month: 9, + day: 25, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, + }, + { + year: 2024, + month: 9, + day: 26, + startMinutesFromMidnight: 450, + endMinutesFromMidnight: 600, + }, + { + year: 2024, + month: 9, + day: 26, + startMinutesFromMidnight: 660, + endMinutesFromMidnight: 840, + }, + { + year: 2024, + month: 9, + day: 26, + startMinutesFromMidnight: 990, + endMinutesFromMidnight: 1230, }, ], + today: { + year: 2024, + month: 9, + day: 20, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -621,13 +758,22 @@ export const expectedLocationData = [ name: "SCOTTY'S MARKET BY SALEM'S", shortDescription: "International and conventional groceries, savory grilled meats and hot meals.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/180", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -636,13 +782,22 @@ export const expectedLocationData = [ name: "STACK'D DESSERT BAR", shortDescription: "Cool down this summer with milkshakes, ice cream sundaes and floats!", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/190", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -651,13 +806,22 @@ export const expectedLocationData = [ name: "STACK'D UNDERGROUND", shortDescription: "Smashed burgers, Nashville-style chicken an gourmet grilled cheese", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/188", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -666,13 +830,22 @@ export const expectedLocationData = [ name: "STEPHANIE'S - MARKET C", shortDescription: "Fresh sandwiches, wraps and salads, snacks, sweets, gourmet coffee and cold beverages", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/148", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -680,13 +853,22 @@ export const expectedLocationData = [ conceptId: 82, name: "TAHINI", shortDescription: "Fresh Mediterranean, Certified Kosher Cuisine", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/82", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -696,34 +878,56 @@ export const expectedLocationData = [ shortDescription: "Campus food truck", description: "The Exchange offers custom deli sandwiches, soups, hot entrées, fresh baked goods, fruit, yogurt parfaits, snack and energy bars, and other grab-and-go items. The designated coffee bar includes hot brewed La Prima coffee, specialty and organic teas, cold beverages, and bottled juices.", + url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/168", location: "Posner Hall, 1st Floor", menu: "https://web.archive.org/web/20240721001349/https://apps.studentaffairs.cmu.edu/dining/dashboard_images/Production/menus/92/menu-exchange-2024-25-v2.pdf", - - url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/168", - coordinates: { lat: 401.441354, lng: -791.942125 }, + coordinates: { + lat: 401.441354, + lng: -791.942125, + }, acceptsOnlineOrders: false, times: [ { - start: { day: 1, hour: 8, minute: 0 }, - end: { day: 1, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 22, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 2, hour: 8, minute: 0 }, - end: { day: 2, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 23, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 3, hour: 8, minute: 0 }, - end: { day: 3, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 24, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 4, hour: 8, minute: 0 }, - end: { day: 4, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 25, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, { - start: { day: 5, hour: 8, minute: 0 }, - end: { day: 5, hour: 15, minute: 0 }, + year: 2024, + month: 7, + day: 26, + startMinutesFromMidnight: 480, + endMinutesFromMidnight: 900, }, ], + today: { + year: 2024, + month: 7, + day: 20, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -731,13 +935,22 @@ export const expectedLocationData = [ conceptId: 114, name: "TASTE OF INDIA", shortDescription: "Taste of India provide a vibrant tastes of India.", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/114", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -746,13 +959,22 @@ export const expectedLocationData = [ name: "TEPPER TAQUERIA", shortDescription: "Mexican-style street tacos, burritos, quesadillas, bowls, and nachos", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/185", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -761,13 +983,22 @@ export const expectedLocationData = [ name: "TRUE BURGER", shortDescription: "Unique, hand-crafted signature sandwiches and smash burgers", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/138", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -775,13 +1006,22 @@ export const expectedLocationData = [ conceptId: 98, name: "URBAN REVOLUTION - GRUBHUB ONLY", shortDescription: "Grubhub-only, featuring fresh-carved rotisserie options", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/98", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -790,13 +1030,22 @@ export const expectedLocationData = [ name: "WILD BLUE SUSHI - RUGE ATRIUM", shortDescription: "Fresh prepared sushi, hot rice bowls, bubble tea and coffee", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/155", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, @@ -805,14 +1054,30 @@ export const expectedLocationData = [ name: "ZEBRA LOUNGE", shortDescription: "hot and iced coffee and espresso drinks, pastries, sushi, bagels, pizza", - description: "", + description: + "At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!\n \n \n Nutritional information can be found at aubonpain.com/nutrition\n .\n \n \n \n To place a catering order online, visit catering.aubonpain.com\n . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com\n . For on-campus assistance, call 412-268-1054.", url: "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/Concept/84", - location: "", + location: "Cohon Center, Second floor", menu: undefined, - coordinates: undefined, - acceptsOnlineOrders: false, + coordinates: { + lat: 40.44401, + lng: -79.942258, + }, + acceptsOnlineOrders: true, times: [], + today: { + year: 2024, + month: 11, + day: 13, + }, todaysSpecials: undefined, todaysSoups: undefined, }, ]; + +export const expectedLocationData2: ILocation[] = expectedLocationData.filter( + (location) => + ["92", "110", "113", "175", "108", "168"].includes( + location.conceptId.toString() + ) +); diff --git a/tests/html/concepts/113-completely-customizable.html b/tests/html/concepts/113-completely-customizable.html new file mode 100644 index 0000000..b315640 --- /dev/null +++ b/tests/html/concepts/113-completely-customizable.html @@ -0,0 +1,96 @@ + + + + + + + + + + Concept + + + + +
+ +
+ +
+ + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + Reload + 🗙 +
+ + + + + \ No newline at end of file diff --git a/tests/html/concepts/all-closed.html b/tests/html/concepts/all-closed.html new file mode 100644 index 0000000..c817fb3 --- /dev/null +++ b/tests/html/concepts/all-closed.html @@ -0,0 +1,224 @@ + + + + + + + + + + + + + Concept + + + + +
+ +
+
+
+
+
+
+
+ arrow_back +
+
Back to Locations
+
+
+
+
+
+
+

AU BON PAIN AT SKIBO CAF É

+ +
+

At Au Bon Pain café bakery, each signature recipe is uniquely crafted. You can enjoy delicious hot or iced coffee and teas, espresso drinks, a variety of cold beverages, soup, a customized made-to-order breakfast or lunch sandwich or salad, or you can grab a pre-made salad, sandwich, wrap, yogurt parfait, fresh fruit or snack. There is always something new to try ... healthy choices, comfort food, indulgent treats … try them all!

+

+
+

+

+ Nutritional information can be found at aubonpain.com/nutrition + . +

+

+
+

+

+ To place a catering order online, visit catering.aubonpain.com + . To pay with a department or organization’s oracle string, please email your order to abpcmu@grcafes.com + . For on-campus assistance, call 412-268-1054. +

+
+

Hours

+
+
    +
  • +
    + Thursday + November 13, CLOSED +
    +
  • +
  • +
    + Friday + November 14, CLOSED +
    +
  • +
  • +
    + Saturday + November 15, CLOSED +
    +
  • +
  • +
    + Sunday + November 16, CLOSED +
    +
  • +
  • +
    + Monday + November 17, CLOSED +
    +
  • +
  • +
    + Tuesday + November 18, CLOSED +
    +
  • +
  • +
    + Wednesday + November 19, CLOSED +
    +
  • +
+
+
+ +
+
+
+
+
+ +
+ +
+ An error has occurred. This application may no longer respond until reloaded. + + + Reload + 🗙 +
+ + + diff --git a/tests/html/concepts/error.html b/tests/html/concepts/error.html new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 65f9c30..d30ba24 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -1,5 +1,5 @@ import DiningParser from "../src/parser/diningParser"; -import { expectedLocationData } from "./expectedData"; +import { expectedLocationData, expectedLocationData2 } from "./expectedData"; import { mockAxiosGETMethodWithFilePaths } from "./mockAxios"; import { setUpTimingTest, @@ -11,24 +11,51 @@ import { Fri, Sat, Sun, + setUpArbitraryTest, } from "./mockTimings"; +import { DateTime } from "luxon"; -jest.mock("axios"); -test("ok", () => {}); -test("the whole thing, including locationOverwrites", async () => { +vi.mock("axios"); + +test("the whole thing", async () => { mockAxiosGETMethodWithFilePaths({ conceptListFilePath: "html/listconcepts.html", specialsFilePath: "html/specials.html", soupsFilePath: "html/soups.html", getConceptFilePath: (conceptId: string) => - ["92", "110", "113", "175", "108", "168"].includes(conceptId) // note that location 168 does not have a location overwrite and thus uses the one provided on the page + ["92", "110", "113", "175", "108", "168"].includes(conceptId) ? `html/concepts/${conceptId}.html` - : "html/blank.html", + : "html/concepts/all-closed.html", + serverDate: DateTime.fromObject({ + year: 2024, + month: 8, + day: 5, + }) as DateTime, }); const parser = new DiningParser(); const parsedLocationData = await parser.process(); + expect(parsedLocationData).toStrictEqual(expectedLocationData); }); +test("the whole thing, with per-page errors", async () => { + mockAxiosGETMethodWithFilePaths({ + conceptListFilePath: "html/listconcepts.html", + specialsFilePath: "html/specials.html", + soupsFilePath: "html/soups.html", + getConceptFilePath: (conceptId: string) => + ["92", "110", "113", "175", "108", "168"].includes(conceptId) + ? `html/concepts/${conceptId}.html` + : "html/concepts/error.html", + serverDate: DateTime.fromObject({ + year: 2024, + month: 8, + day: 5, + }) as DateTime, + }); + const parser = new DiningParser(); + const parsedLocationData = await parser.process(); + expect(parsedLocationData).toStrictEqual(expectedLocationData2); +}); test("specials for The Exchange", async () => { mockAxiosGETMethodWithFilePaths({ conceptListFilePath: "html/listconcepts.html", @@ -37,7 +64,12 @@ test("specials for The Exchange", async () => { getConceptFilePath: (conceptId: string) => ["92", "110", "113", "175", "108"].includes(conceptId) ? `html/concepts/${conceptId}.html` - : "html/blank.html", + : "html/concepts/error.html", + serverDate: DateTime.fromObject({ + year: 2024, + month: 8, + day: 5, + }) as DateTime, }); const parser = new DiningParser(); expect((await parser.process()).map((data) => data.todaysSpecials)).toEqual( @@ -64,7 +96,13 @@ test("specials for The Exchange", async () => { test( "parser throws on repeated axios error", async () => { - mockAxiosGETMethodWithFilePaths({}); + mockAxiosGETMethodWithFilePaths({ + serverDate: DateTime.fromObject({ + year: 2024, + month: 8, + day: 5, + }) as DateTime, + }); const parser = new DiningParser(); await expect(async () => { @@ -83,7 +121,12 @@ describe("time edge cases", () => { [Thur]: "24 hRs", [Fri]: "24 hours", }); - await queryParserAndAssertTimingsCorrect([[Tue, 0, 0, Fri, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Tue, 0, 0, 23, 59], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + ]); }); test("all day every day", async () => { setUpTimingTest({ @@ -95,7 +138,15 @@ describe("time edge cases", () => { [Sat]: "24 hours", [Sun]: "24 hours", }); - await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this is the reason why we leave the other return type that represents open every day as this. (backwards compatibility, mostly) + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Tue, 0, 0, 23, 59], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 23, 59], + [Sun, 0, 0, 23, 59], + ]); }); test("all day every day but slightly different", async () => { setUpTimingTest({ @@ -107,7 +158,16 @@ describe("time edge cases", () => { [Sat]: "12:00 AM - 2:59 AM, 3:00 AM - 11:59 PM", [Sun]: "24 hours", }); - await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Tue, 0, 0, 23, 59], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 2, 59], + [Sat, 3, 0, 23, 59], + [Sun, 0, 0, 23, 59], + ]); }); test("empty string", async () => { setUpTimingTest({ @@ -119,7 +179,10 @@ describe("time edge cases", () => { [Sat]: "", [Sun]: "24 hours", }); - await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Mon, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Sun, 0, 0, 23, 59], + ]); }); test("loop-back time coalescing (wrapping on saturday, but it overlaps with sunday)", async () => { setUpTimingTest({ @@ -131,7 +194,10 @@ describe("time edge cases", () => { [Sat]: "7:00 AM - 2:00 AM", [Sun]: "1:00 AM - 5:00 PM", // sunday is represented as 0 }); - await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 17, 0]]); + await queryParserAndAssertTimingsCorrect([ + [Sat, 7, 0, 2, 0], + [Sun, 1, 0, 17, 0], + ]); }); test("loop-back time coalescing (wrapping on saturday, but it overlaps with multiple ranges on sunday)", async () => { setUpTimingTest({ @@ -141,9 +207,13 @@ describe("time edge cases", () => { [Thur]: "", [Fri]: "", [Sat]: "7:00 AM - 2:00 AM", - [Sun]: "12:00AM - 12:35 AM, 1:00 AM - 5:00 PM", // sunday is represented as 0 + [Sun]: "12:00 AM - 12:35 AM, 1:00 AM - 5:00 PM", // sunday is represented as 0 }); - await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 17, 0]]); + await queryParserAndAssertTimingsCorrect([ + [Sat, 7, 0, 2, 0], + [Sun, 0, 0, 0, 35], + [Sun, 1, 0, 17, 0], + ]); }); test("open all week, gone wrong", async () => { setUpTimingTest({ @@ -155,7 +225,16 @@ describe("time edge cases", () => { [Fri]: "OPEN 24 HOURS", [Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 2:00 AM", }); - await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this should be the default return value if it's open all week + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Tue, 0, 0, 23, 59], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 10, 0], + [Sat, 9, 0, 2, 0], + [Sun, 0, 0, 23, 59], + ]); // this should be the default return value if it's open all week }); test("open all week, gone wrong", async () => { setUpTimingTest({ @@ -167,7 +246,17 @@ describe("time edge cases", () => { [Fri]: "OPEN 24 HOURS", [Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 2:00 AM", }); - await queryParserAndAssertTimingsCorrect([[Sun, 0, 0, Sat, 23, 59]]); // this should be the default return value if it's open all week + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Tue, 0, 0, 23, 59], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 10, 0], + [Sat, 9, 0, 2, 0], + [Sun, 0, 0, 0, 5], + [Sun, 0, 10, 23, 59], + ]); // this should be the default return value if it's open all week }); test("wrapping on thursday, but it overlaps with friday", async () => { setUpTimingTest({ @@ -179,7 +268,10 @@ describe("time edge cases", () => { [Sat]: "", [Sun]: "", }); - await queryParserAndAssertTimingsCorrect([[Thur, 7, 0, Fri, 17, 0]]); + await queryParserAndAssertTimingsCorrect([ + [Thur, 7, 0, 2, 0], + [Fri, 1, 0, 17, 0], + ]); }); test("some combination of wrap-around", async () => { setUpTimingTest({ @@ -192,9 +284,10 @@ describe("time edge cases", () => { [Sun]: "open 24 hours", }); await queryParserAndAssertTimingsCorrect([ - [Sun, 0, 0, Sun, 23, 59], - [Wed, 0, 0, Wed, 23, 59], - [Thur, 7, 0, Fri, 17, 0], + [Wed, 0, 0, 23, 59], + [Thur, 7, 0, 2, 0], + [Fri, 1, 0, 17, 0], + [Sun, 0, 0, 23, 59], ]); }); test("open nearly all week, but Dining Services has truly lost it", async () => { @@ -207,7 +300,20 @@ describe("time edge cases", () => { [Fri]: "OPEN 24 HOURS, mooo", [Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 7:05 AM", }); - await queryParserAndAssertTimingsCorrect([[Sun, 9, 0, Sun, 7, 5]]); + await queryParserAndAssertTimingsCorrect([ + [Sun, 0, 0, 0, 5], + [Sun, 9, 0, 23, 59], + [Mon, 0, 0, 3, 5], + [Mon, 3, 0, 2, 0], + [Tue, 1, 0, 21, 0], + [Tue, 21, 1, 23, 59], + [Tue, 0, 0, 15, 0], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 10, 0], + [Sat, 9, 0, 7, 5], + ]); }); // tests literally everything test("open nearly all week, but Dining Services has truly lost it", async () => { setUpTimingTest({ @@ -220,8 +326,18 @@ describe("time edge cases", () => { [Sat]: "12:00 AM - 10:00 AM, 9:00 AM - 12:02 AM", }); await queryParserAndAssertTimingsCorrect([ - [Sun, 0, 5, Sun, 0, 10], - [Sun, 9, 0, Sun, 0, 2], + [Sun, 0, 5, 0, 10], + [Sun, 9, 0, 23, 59], + [Mon, 0, 0, 3, 5], + [Mon, 3, 0, 2, 0], + [Tue, 1, 0, 21, 0], + [Tue, 21, 1, 23, 59], + [Tue, 0, 0, 15, 0], + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + [Sat, 0, 0, 10, 0], + [Sat, 9, 0, 0, 2], ]); }); // tests literally everything test("degenerate open times", async () => { @@ -235,8 +351,8 @@ describe("time edge cases", () => { [Sun]: "", }); await queryParserAndAssertTimingsCorrect([ - [Thur, 2, 0, Thur, 2, 0], - [Fri, 1, 0, Fri, 1, 0], + [Thur, 2, 0, 2, 0], + [Fri, 1, 0, 1, 0], ]); }); test("single time", async () => { @@ -250,20 +366,23 @@ describe("time edge cases", () => { [Sat]: "2:00 PM - 10:00 PM", }); await queryParserAndAssertTimingsCorrect([ - [Sun, 15, 12, Sun, 23, 30], - [Mon, 9, 0, Mon, 17, 0], - [Tue, 10, 0, Tue, 18, 0], - [Wed, 11, 0, Wed, 19, 0], - [Thur, 12, 0, Thur, 20, 0], - [Fri, 13, 0, Fri, 21, 0], - [Sat, 14, 0, Sat, 22, 0], + [Sun, 15, 12, 23, 30], + [Mon, 9, 0, 17, 0], + [Tue, 10, 0, 18, 0], + [Wed, 11, 0, 19, 0], + [Thur, 12, 0, 20, 0], + [Fri, 13, 0, 21, 0], + [Sat, 14, 0, 22, 0], ]); }); test("duplicated time string", async () => { setUpTimingTest({ [Sun]: "9:00 AM - 4:00 PM, 9:00 AM - 4:00 PM", }); - await queryParserAndAssertTimingsCorrect([[Sun, 9, 0, Sun, 16, 0]]); + await queryParserAndAssertTimingsCorrect([ + [Sun, 9, 0, 16, 0], + [Sun, 9, 0, 16, 0], + ]); // we keep all duplicates as-is now }); test("gap between time strings", async () => { @@ -275,13 +394,14 @@ describe("time edge cases", () => { }); await queryParserAndAssertTimingsCorrect([ - [Sun, 11, 0, Sun, 14, 0], - [Sun, 15, 0, Sun, 15, 1], - [Sun, 16, 0, Sun, 21, 0], - [Thur, 7, 0, Thur, 22, 0], - [Fri, 11, 0, Fri, 16, 0], - [Sat, 11, 0, Sat, 14, 0], - [Sat, 16, 0, Sat, 21, 0], + [Sun, 11, 0, 14, 0], + [Sun, 15, 0, 15, 1], + [Sun, 16, 0, 21, 0], + [Thur, 7, 0, 22, 0], + [Fri, 11, 0, 16, 0], + [Fri, 11, 0, 16, 0], + [Sat, 11, 0, 14, 0], + [Sat, 16, 0, 21, 0], ]); }); test("12AM (tests the 12:00 AM -> 11:59 PM shift)", async () => { @@ -292,10 +412,10 @@ describe("time edge cases", () => { [Thur]: "6:00 PM - 12:00 AM", }); await queryParserAndAssertTimingsCorrect([ - [Mon, 0, 0, Mon, 23, 59], - [Tue, 2, 0, Tue, 23, 59], - [Wed, 11, 0, Wed, 23, 59], - [Thur, 18, 0, Thur, 23, 59], + [Mon, 0, 0, 23, 59], + [Tue, 2, 0, 23, 59], + [Wed, 11, 0, 23, 59], + [Thur, 18, 0, 23, 59], ]); }); test("same and different opening and closing times", async () => { @@ -306,10 +426,14 @@ describe("time edge cases", () => { [Sat]: "9:00 AM - 4:00 PM, 8:00 AM - 4:00 PM", }); await queryParserAndAssertTimingsCorrect([ - [Wed, 8, 0, Wed, 16, 0], - [Thur, 8, 0, Thur, 16, 0], - [Fri, 8, 0, Fri, 16, 0], - [Sat, 8, 0, Sat, 16, 0], + [Wed, 8, 0, 16, 0], + [Wed, 8, 0, 14, 0], + [Thur, 8, 0, 16, 0], + [Thur, 8, 0, 14, 0], + [Fri, 8, 0, 16, 0], + [Fri, 9, 0, 16, 0], + [Sat, 9, 0, 16, 0], + [Sat, 8, 0, 16, 0], ]); }); @@ -322,12 +446,17 @@ describe("time edge cases", () => { [Fri]: "7:00 AM - 4:00 PM, 6:00 AM - 2:00 PM, 7:00 PM - 12:00 AM", }); await queryParserAndAssertTimingsCorrect([ - [Mon, 8, 0, Mon, 21, 0], - [Tue, 8, 0, Tue, 21, 0], - [Wed, 8, 0, Wed, 21, 0], - [Thur, 8, 0, Thur, 21, 0], - [Fri, 6, 0, Fri, 16, 0], - [Fri, 19, 0, Fri, 23, 59], + [Mon, 8, 0, 16, 0], + [Mon, 14, 0, 21, 0], + [Tue, 14, 0, 21, 0], + [Tue, 8, 0, 16, 0], + [Wed, 8, 0, 21, 0], + [Wed, 14, 0, 16, 0], + [Thur, 14, 0, 16, 0], + [Thur, 8, 0, 21, 0], + [Fri, 7, 0, 16, 0], + [Fri, 6, 0, 14, 0], + [Fri, 19, 0, 23, 59], ]); }); test("partial all day", async () => { @@ -336,7 +465,11 @@ describe("time edge cases", () => { [Thur]: "open 24 hours", [Fri]: "open 24 hours", }); - await queryParserAndAssertTimingsCorrect([[Wed, 0, 0, Fri, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Wed, 0, 0, 23, 59], + [Thur, 0, 0, 23, 59], + [Fri, 0, 0, 23, 59], + ]); }); test("partial all day, over the weekend", async () => { setUpTimingTest({ @@ -344,7 +477,11 @@ describe("time edge cases", () => { [Sun]: "open 24 hours", [Mon]: "open 24 hours", }); - await queryParserAndAssertTimingsCorrect([[Sat, 0, 0, Mon, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Sat, 0, 0, 23, 59], + [Sun, 0, 0, 23, 59], + [Mon, 0, 0, 23, 59], + ]); }); test("partial all day, over the weekend", async () => { setUpTimingTest({ @@ -352,13 +489,17 @@ describe("time edge cases", () => { [Sun]: "open 24 hours", [Mon]: "open 24 hours", }); - await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Mon, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Sat, 7, 0, 0, 1], + [Sun, 0, 0, 23, 59], + [Mon, 0, 0, 23, 59], + ]); }); test("another one", async () => { setUpTimingTest({ [Sat]: "7:00 AM - 12:01 AM", }); - await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, Sun, 0, 1]]); + await queryParserAndAssertTimingsCorrect([[Sat, 7, 0, 0, 1]]); }); test("unparseable token", async () => { setUpTimingTest({ @@ -370,6 +511,37 @@ describe("time edge cases", () => { setUpTimingTest({ [Mon]: "OPEN 24 HOURS, 2:00 AM - 3:00 AM", }); - await queryParserAndAssertTimingsCorrect([[Mon, 0, 0, Mon, 23, 59]]); + await queryParserAndAssertTimingsCorrect([ + [Mon, 0, 0, 23, 59], + [Mon, 2, 0, 3, 0], + ]); + }); +}); +describe("new year parsing", () => { + test("new years", async () => { + setUpArbitraryTest( + [ + ["Tuesday", "December 30", "7:00 AM - 9:00 PM"], + ["Wednesday", "December 31", "7:00 AM - 9:00 PM"], + ["Thursday", "January 1", "7:00 AM - 9:00 PM"], + ["Friday", "January 2", "7:00 AM - 9:00 PM"], + ["Saturday", "January 3", "7:00 AM - 9:00 PM"], + ["Sunday", "January 4", "7:00 AM - 9:00 PM"], + ["Monday", "January 5", "7:00 AM - 9:00 PM"], + ], + DateTime.fromObject({ year: 2025, month: 12, day: 30 }) + ); + await queryParserAndAssertTimingsCorrect( + [ + [Sun, 7, 0, 21, 0], + [Mon, 7, 0, 21, 0], + [Tue, 7, 0, 21, 0], + [Wed, 7, 0, 21, 0], + [Thur, 7, 0, 21, 0], + [Fri, 7, 0, 21, 0], + [Sat, 7, 0, 21, 0], + ], + DateTime.fromObject({ year: 2025, month: 12, day: 30 }) + ); }); }); diff --git a/tests/locationMerger.test.ts b/tests/locationMerger.test.ts index 1b682f1..630799e 100644 --- a/tests/locationMerger.test.ts +++ b/tests/locationMerger.test.ts @@ -1,5 +1,7 @@ -import LocationMerger from "../src/utils/locationMerger"; -const locationA = { +import { ILocation } from "types"; +import ScrapeResultMerger from "../src/utils/locationMerger"; +import { DeeplyAllowMatchers } from "vitest"; +const locationA: ILocation = { conceptId: 3, name: "Location Name", description: "Location Description", @@ -7,8 +9,14 @@ const locationA = { location: "Location Address", times: [], acceptsOnlineOrders: true, + shortDescription: undefined, + coordinates: undefined, + today: { day: 1, month: 1, year: 1 }, + menu: undefined, + todaysSoups: undefined, + todaysSpecials: undefined, }; -const locationAA = { +const locationAA: ILocation = { conceptId: 3, name: "Location Name", description: "Location Description", @@ -16,8 +24,14 @@ const locationAA = { location: "Location Address Changed", times: [], acceptsOnlineOrders: true, + shortDescription: undefined, + coordinates: undefined, + today: { day: 1, month: 1, year: 1 }, + menu: undefined, + todaysSoups: undefined, + todaysSpecials: undefined, }; -const locationAAA = { +const locationAAA: ILocation = { conceptId: 3, name: "Location Name", description: "Location Description", @@ -25,8 +39,14 @@ const locationAAA = { location: "Location Address Changed", times: [], acceptsOnlineOrders: false, + shortDescription: undefined, + coordinates: undefined, + today: { day: 1, month: 1, year: 1 }, + menu: undefined, + todaysSoups: undefined, + todaysSpecials: undefined, }; -const locationB = { +const locationB: ILocation = { conceptId: 2, name: "Location Name", description: "Location Description", @@ -34,15 +54,24 @@ const locationB = { location: "Location Address", times: [], acceptsOnlineOrders: true, + shortDescription: undefined, + coordinates: undefined, + today: { day: 1, month: 1, year: 1 }, + menu: undefined, + todaysSoups: undefined, + todaysSpecials: undefined, }; // https://stackoverflow.com/questions/40135684/is-there-an-array-equality-match-function-that-ignores-element-position-in-jest -const expectArrayEquivalence = (actual: T[], expected: T[]) => { +const expectArrayEquivalence = ( + actual: DeeplyAllowMatchers[], + expected: DeeplyAllowMatchers[] +) => { expect(actual).toEqual(expect.arrayContaining(expected)); expect(expected).toEqual(expect.arrayContaining(actual)); }; describe("merging", () => { test("", () => { - const merger = new LocationMerger(); + const merger = new ScrapeResultMerger(); merger.addLocation(locationA); merger.addLocation(locationAA); merger.addLocation(locationAAA); @@ -54,7 +83,7 @@ describe("merging", () => { ]); }); test("highly necessary test", () => { - const merger = new LocationMerger(); + const merger = new ScrapeResultMerger(); merger.addLocation(locationA); merger.addLocation(locationAA); merger.addLocation(locationAAA); diff --git a/tests/mockAxios.ts b/tests/mockAxios.ts index dd20ef1..cedf1f2 100644 --- a/tests/mockAxios.ts +++ b/tests/mockAxios.ts @@ -1,5 +1,7 @@ +import { DateTime } from "luxon"; import { getFileContent, last } from "./utils"; import axios from "axios"; +import { Mock } from "vitest"; const ALL_LOCATIONS_URL = "https://apps.studentaffairs.cmu.edu/dining/conceptinfo/?page=listConcepts"; @@ -19,14 +21,19 @@ export function mockAxiosGETMethod({ specialsHTML, soupsHTML, conceptHTML, + serverDate, }: { - conceptListHTML?: string; - specialsHTML?: string; - soupsHTML?: string; - conceptHTML?: (id: string) => string | undefined; + conceptListHTML?: string | undefined; + specialsHTML?: string | undefined; + soupsHTML?: string | undefined; + conceptHTML?: ((id: string) => string | undefined) | undefined; + serverDate: DateTime; }) { - (axios.get as jest.Mock).mockImplementation(async (url: string) => { - return { data: getHTML(url) }; + (axios.get as Mock).mockImplementation(async (url: string) => { + return { + data: getHTML(url), + headers: { date: serverDate.toRFC2822() }, + }; }); const getHTML = (url: string) => { @@ -35,7 +42,7 @@ export function mockAxiosGETMethod({ if (url === SPECIALS_URL && specialsHTML !== undefined) return specialsHTML; if (url === SOUPS_URL && soupsHTML !== undefined) return soupsHTML; if (url.startsWith(LOCATION_URL_PREFIX) && conceptHTML !== undefined) - return conceptHTML(last(url.split("/"))); + return conceptHTML(last(url.split("/"))!); throw new Error(`url ${url} not found!`); }; } @@ -48,11 +55,13 @@ export function mockAxiosGETMethodWithFilePaths({ specialsFilePath, soupsFilePath, getConceptFilePath, + serverDate, }: { conceptListFilePath?: string; specialsFilePath?: string; soupsFilePath?: string; getConceptFilePath?: (conceptId: string) => string; + serverDate: DateTime; }) { mockAxiosGETMethod({ conceptListHTML: getFileContent(conceptListFilePath), @@ -62,5 +71,6 @@ export function mockAxiosGETMethodWithFilePaths({ getConceptFilePath ? getFileContent(getConceptFilePath(conceptId)) : undefined, + serverDate, }); } diff --git a/tests/mockTimings.ts b/tests/mockTimings.ts index c5b8b14..87cf2b2 100644 --- a/tests/mockTimings.ts +++ b/tests/mockTimings.ts @@ -1,70 +1,56 @@ +import { DateTime } from "luxon"; import DiningParser from "../src/parser/diningParser"; import { mockAxiosGETMethod } from "./mockAxios"; import { getFileContent } from "./utils"; +import { IFullTimeRange } from "types"; -enum DayOfTheWeek { - SUNDAY = "SUNDAY", - MONDAY = "MONDAY", - TUESDAY = "TUESDAY", - WEDNESDAY = "WEDNESDAY", - THURSDAY = "THURSDAY", - FRIDAY = "FRIDAY", - SATURDAY = "SATURDAY", -} -export const Mon = DayOfTheWeek.MONDAY, - Tue = DayOfTheWeek.TUESDAY, - Wed = DayOfTheWeek.WEDNESDAY, - Thur = DayOfTheWeek.THURSDAY, - Fri = DayOfTheWeek.FRIDAY, - Sat = DayOfTheWeek.SATURDAY, - Sun = DayOfTheWeek.SUNDAY; +export const Mon = 1, + Tue = 2, + Wed = 3, + Thur = 4, + Fri = 5, + Sat = 6, + Sun = 0; +function _sort(a: IFullTimeRange, b: IFullTimeRange) { + if (a.day !== b.day) return a.day - b.day; + if (a.startMinutesFromMidnight !== b.startMinutesFromMidnight) + return a.startMinutesFromMidnight - b.startMinutesFromMidnight; + return a.endMinutesFromMidnight - b.endMinutesFromMidnight; +} /** * * @param times [startDay,startHour,startMinute,endDay,endHour,endMinute][] (order matters for input! Sunday comes first) * The only reason start and end are bundled into one array is because prettier will autoformat it to two lines otherwise */ export async function queryParserAndAssertTimingsCorrect( - times: [DayOfTheWeek, number, number, DayOfTheWeek, number, number][] + times: [number, number, number, number, number][], + rootDay: DateTime = DateTime.fromObject({ year: 2024, month: 9, day: 9 }) // is the default for most of the tests ) { - const parser = new DiningParser(); - const result = await parser.process(); + const result = await new DiningParser().process(); expect(result.length).toBe(1); - expect(result[0].times).toStrictEqual( - times.map((time) => { - return { - start: { - day: mapDayOfWeekToAPIReturnValue(time[0]), - hour: time[1], - minute: time[2], - }, - end: { - day: mapDayOfWeekToAPIReturnValue(time[3]), - hour: time[4], - minute: time[5], - }, - }; - }) + expect(result[0]!.times.sort(_sort)).toStrictEqual( + times + .map((time) => { + const day = rootDay.plus({ days: (time[0] - Mon + 7) % 7 }); + return { + year: day.year, + month: day.month, + day: day.day, + startMinutesFromMidnight: time[1] * 60 + time[2], + endMinutesFromMidnight: time[3] * 60 + time[4], + }; + }) + .sort(_sort) ); + expect(result[0]?.today).toEqual({ + year: rootDay.year, + month: rootDay.month, + day: rootDay.day, + }); } -function mapDayOfWeekToAPIReturnValue(day: DayOfTheWeek) { - return ( - { - [Sun]: 0, - [Mon]: 1, - [Tue]: 2, - [Wed]: 3, - [Thur]: 4, - [Fri]: 5, - [Sat]: 6, - } satisfies Record - )[day]; -} - -export function setUpTimingTest( - timeRows: Partial> -) { +export function setUpTimingTest(timeRows: Record) { const fillInHTMLWithTimes = (html: string) => { html = html .replace("[MONDAY]", timeRows[Mon] ?? "CLOSED") @@ -86,5 +72,51 @@ export function setUpTimingTest( getFileContent("html/concepts/113-tests.html") ); }, + serverDate: DateTime.fromObject({ + year: 2024, + month: 9, + day: 9, + }) as DateTime, // this was the date when the test page was scraped + }); +} + +/** + * concept id is 113 + * @param data + * @param serverDate (time the server "processed" the request. This value matters for overwrite times) + */ +export function setUpArbitraryTest( + data: [ + [string, string, string], + [string, string, string], + [string, string, string], + [string, string, string], + [string, string, string], + [string, string, string], + [string, string, string] + ], + serverDate: DateTime +) { + const fillInHtml = (html: string) => { + const A_CHAR_CODE = "A".charCodeAt(0); + for (let i = 0; i < 7; i++) { + for (let j = 0; j < 3; j++) { + const square = `[${String.fromCharCode(A_CHAR_CODE + i)}${j + 1}]`; + html = html.replace(square, data[i]![j]!); + } + } + return html; + }; + mockAxiosGETMethod({ + conceptListHTML: getFileContent("html/listconcepts-just-113.html"), + soupsHTML: getFileContent("html/blank.html"), + specialsHTML: getFileContent("html/blank.html"), + conceptHTML: (id) => { + expect(id).toBe("113"); + return fillInHtml( + getFileContent("html/concepts/113-completely-customizable.html") + ); + }, + serverDate, }); } diff --git a/tests/postgresClient.ts b/tests/postgresClient.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json index d440c50..141b8e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "es6", "skipLibCheck": true, - "types": ["jest", "node"], + "types": ["node", "vitest/globals"], /* Bundler mode */ "rootDir": ".", @@ -24,7 +24,9 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, // this should honestly be the default, since you're not guaranteed to get a value when you key into an object + "exactOptionalPropertyTypes": true // | undefined needs to be explicit }, "include": ["src", "tests"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..59d6604 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + test: { + globals: true, + testTimeout: 30_000, + coverage: { + provider: "v8", + reporter: ["text", "json-summary", "json"], + }, + }, + plugins: [tsconfigPaths()], +});