diff --git a/README.md b/README.md index 2b40bbf..561b693 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,15 @@ 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`, `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) +Then start your local database with (assuming you have the correct env variables) + +```bash +pnpm db:start +pnpm db:push # if this is your first time running the db +pnpm dev +``` + +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!) diff --git a/drizzle/0010_shallow_doctor_strange.sql b/drizzle/0010_shallow_doctor_strange.sql new file mode 100644 index 0000000..dd9e60a --- /dev/null +++ b/drizzle/0010_shallow_doctor_strange.sql @@ -0,0 +1,10 @@ +CREATE TABLE "reports" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "reports_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "location_id" text NOT NULL, + "message" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "reports" ADD CONSTRAINT "reports_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reports" ADD CONSTRAINT "reports_location_id_location_data_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."location_data"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..ced89bc --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1115 @@ +{ + "id": "f2f47465-b027-4494-b217-1e6ca9c4ce8e", + "prevId": "99fa16e2-e656-421e-aad6-02437afea8ce", + "version": "7", + "dialect": "postgresql", + "tables": { + "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.external_id_to_internal_id": { + "name": "external_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 + }, + "external_id_type": { + "name": "external_id_type", + "type": "externalIdType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'concept_id'" + } + }, + "indexes": { + "internal_id": { + "name": "internal_id", + "columns": [ + { + "expression": "internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_id_to_internal_id_internal_id_location_data_id_fk": { + "name": "external_id_to_internal_id_internal_id_location_data_id_fk", + "tableFrom": "external_id_to_internal_id", + "tableTo": "location_data", + "columnsFrom": [ + "internal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "external_id_to_internal_id_external_id_unique": { + "name": "external_id_to_internal_id_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + } + }, + "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.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reports_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reports_user_id_users_id_fk": { + "name": "reports_user_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reports_location_id_location_data_id_fk": { + "name": "reports_location_id_location_data_id_fk", + "tableFrom": "reports", + "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.star_reviews": { + "name": "star_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "star_reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "star_rating": { + "name": "star_rating", + "type": "numeric(2, 1)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "star_reviews_location_user_uniq": { + "name": "star_reviews_location_user_uniq", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "star_reviews_user_id_users_id_fk": { + "name": "star_reviews_user_id_users_id_fk", + "tableFrom": "star_reviews", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "star_reviews_location_id_location_data_id_fk": { + "name": "star_reviews_location_id_location_data_id_fk", + "tableFrom": "star_reviews", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "rating_number_check": { + "name": "rating_number_check", + "value": "\"star_reviews\".\"star_rating\" > 0 AND \"star_reviews\".\"star_rating\" <= 5 AND mod(\"star_reviews\".\"star_rating\"*2,1) = 0" + } + }, + "isRLSEnabled": false + }, + "public.tag_list": { + "name": "tag_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tag_list_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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag_reviews": { + "name": "tag_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tag_reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "written_review": { + "name": "written_review", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_reviews_location_tag_user_uniq": { + "name": "tag_reviews_location_tag_user_uniq", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tag_reviews_tag_id_tag_list_id_fk": { + "name": "tag_reviews_tag_id_tag_list_id_fk", + "tableFrom": "tag_reviews", + "tableTo": "tag_list", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tag_reviews_user_id_users_id_fk": { + "name": "tag_reviews_user_id_users_id_fk", + "tableFrom": "tag_reviews", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tag_reviews_location_id_location_data_id_fk": { + "name": "tag_reviews_location_id_location_data_id_fk", + "tableFrom": "tag_reviews", + "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 + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "google_id": { + "name": "google_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture_url": { + "name": "picture_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "google_id": { + "name": "google_id", + "columns": [ + { + "expression": "google_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_time_overwrites_table": { + "name": "weekly_time_overwrites_table", + "schema": "", + "columns": { + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weekday": { + "name": "weekday", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "time_string": { + "name": "time_string", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "weekly_time_overwrites_table_location_id_location_data_id_fk": { + "name": "weekly_time_overwrites_table_location_id_location_data_id_fk", + "tableFrom": "weekly_time_overwrites_table", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "weekly_time_overwrites_table_location_id_weekday_pk": { + "name": "weekly_time_overwrites_table_location_id_weekday_pk", + "columns": [ + "location_id", + "weekday" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "weekday_check": { + "name": "weekday_check", + "value": "\"weekly_time_overwrites_table\".\"weekday\" >= 0 AND \"weekly_time_overwrites_table\".\"weekday\" < 7" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.externalIdType": { + "name": "externalIdType", + "schema": "public", + "values": [ + "concept_id" + ] + }, + "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 1d6beab..46b2076 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1769646373776, "tag": "0009_cooing_snowbird", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1771716042342, + "tag": "0010_shallow_doctor_strange", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts index 9315a26..3639aef 100644 --- a/src/db/dbQueryUtils.ts +++ b/src/db/dbQueryUtils.ts @@ -1,9 +1,11 @@ +import { avg, count, sql } from "drizzle-orm"; import { externalIdToInternalIdTable, emailTable, locationDataTable, overwritesTable, specialsTable, + starReviewTable, timeOverwritesTable, timesTable, weeklyTimeOverwritesTable, @@ -183,6 +185,33 @@ export class QueryUtils { return { idToPointOverrides, idToWeeklyOverrides }; } + async getRatingsAvgsAndCounts(): Promise< + [Record, Record] + > { + const ratings = await this.db + .select({ + starRating: + // since all ratings are from 0 to 5, the whole part of the average cannot + // exceeds one. also we make the decimal digits longer for other consumers + sql`cast(${avg(starReviewTable.starRating)} as decimal(4,3))`.mapWith( + Number, + ), + count: count(starReviewTable.id).mapWith(Number), + locationId: starReviewTable.locationId, + }) + .from(starReviewTable) + .groupBy(starReviewTable.locationId); + + const ratingsAvgs = Object.fromEntries( + ratings.map((e) => [e.locationId, e.starRating]), + ); + const ratingsCounts = Object.fromEntries( + ratings.map((e) => [e.locationId, e.count]), + ); + + return [ratingsAvgs, ratingsCounts]; + } + async getEmails(): Promise<{ name: string; email: string }[]> { const result = await this.db .select({ diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts index f73046b..f6d932e 100644 --- a/src/db/getLocations.ts +++ b/src/db/getLocations.ts @@ -22,6 +22,7 @@ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { const { idToPointOverrides, idToWeeklyOverrides } = await DB.getTimeOverrides( timeSearchCutoff.toSQLDate(), ); + const [ratingsAvgs, ratingsCounts] = await DB.getRatingsAvgsAndCounts(); // apply overrides, merge all time intervals, and add specials const finalLocationData = Object.entries(locationIdToData).map( @@ -43,6 +44,8 @@ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { // time.end // ).toLocaleString()}` // ), + ratingsAvg: ratingsAvgs[id] ?? null, + ratingsCount: ratingsCounts[id] ?? 0, todaysSoups: specials[id]?.soups ?? [], todaysSpecials: specials[id]?.specials ?? [], }; diff --git a/src/db/schema.ts b/src/db/schema.ts index a00067b..1863f01 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -229,3 +229,24 @@ export const starReviewTable = pgTable( ), // rating is a multiple of .5 ], ); + +export const reportsTable = pgTable( + "reports", + { + id: integer("id").generatedAlwaysAsIdentity().primaryKey(), + userId: integer("user_id") + .references(() => userTable.id, { + onDelete: "cascade", + }), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "date", + }).notNull().defaultNow(), + locationId: text("location_id") + .references(() => locationDataTable.id, { + onDelete: "cascade", + }) + .notNull(), + message: text("message").notNull(), + } +) diff --git a/src/endpoints/misc.ts b/src/endpoints/misc.ts index 74c9fda..9590e1f 100644 --- a/src/endpoints/misc.ts +++ b/src/endpoints/misc.ts @@ -6,6 +6,11 @@ import { DateTime } from "luxon"; import { notifySlack } from "utils/slack"; import { LocationsSchema } from "./schemas"; import { env } from "env"; +import { eq } from "drizzle-orm" + +import { locationDataTable, reportsTable } from "db/schema"; +import { fetchUserDetails } from "./auth"; + import { sendEmail } from "utils/email"; export const miscEndpoints = new Elysia(); @@ -52,23 +57,58 @@ miscEndpoints.post( detail: { hide: true }, }, ); + miscEndpoints.post( "/report", - async ({ body: { message, locationId, locationName } }) => { - runBackgroundJobForErrorReport({ locationName, locationId, message }).catch( - console.error, - ); - return {}; + async ({ cookie, body: { locationId, message } }) => { + const session = cookie["session_id"]!.value as string | undefined; + const userDetails = await fetchUserDetails(session); + + const userId = userDetails?.id; + + const reports = await db.select().from(locationDataTable).where(eq(locationDataTable.id, locationId)) + if (reports.length == 0) { + throw new Response(`Invalid location id ${locationId}`, { + status: 400, + }); + } + + if (reports.length > 1) { + throw new Response(` + Expected 1 restaurant corresponding to id=${locationId}. Somehow got 2. + `, { status: 500 }) // this should be unreachable + } + + const locationName = reports[0]?.name ?? "Unnamed" + createReport( + { + locationName, + locationId, + message, + } + ).catch(console.error) + + await db.insert(reportsTable).values({ + locationId, + message, + userId, + }) + + return {} }, { body: t.Object({ - locationName: t.String(), locationId: t.String(), - message: t.String({ maxLength: 300, minLength: 1 }), + message: t.String({ minLength: 1, maxLength: 512 }), }), - }, + detail: { + description: + "Endpoint for reporting errors in information", + }, + } ); -async function runBackgroundJobForErrorReport({ + +async function createReport({ locationName, locationId, message, @@ -81,10 +121,11 @@ async function runBackgroundJobForErrorReport({ env.ALERT_EMAIL_SEND, env.ALERT_EMAIL_CC, `[CMU Eats] Report for ${locationName}`, - `${message}\n\nBest,\nCMU Eats team`, + `${message}\n\nBest,\nCMU Eats automated report system`, ); await notifySlack( `Report for ${locationName} (\`${locationId}\`): ${message} \nEmailed: ${received.join(", ")}`, env.SLACK_MAIN_CHANNEL_WEBHOOK_URL, ); + } diff --git a/src/endpoints/reviews.ts b/src/endpoints/reviews.ts index 7198609..4dbb393 100644 --- a/src/endpoints/reviews.ts +++ b/src/endpoints/reviews.ts @@ -1,5 +1,6 @@ import Elysia, { status, t } from "elysia"; import { fetchUserDetails } from "./auth"; +import { eq } from "drizzle-orm" import { addStarReview, deleteStarReview, @@ -9,6 +10,7 @@ import { updateTagReview, } from "db/reviews"; import { db } from "db/db"; +import { reportsTable } from "db/schema"; export const reviewEndpoints = new Elysia(); reviewEndpoints @@ -40,7 +42,7 @@ reviewEndpoints buckets: t.Array(t.Number(), { example: [0, 1, 0, 4, 12, 4], description: - "Count of ratings of star rating [{.5},{1,1.5},{2,2.5},{3,3.5},{4,4.5},{5}", + "Count of ratings of star rating [{.5},{1,1.5},{2,2.5},{3,3.5},{4,4.5},{5}]", }), }), tagData: t.Array( @@ -127,4 +129,23 @@ reviewEndpoints }) ), } + ) + .get( + "/v2/locations/:locationId/reports", + async ({ params: { locationId } }) => { + const reports = await db.select().from(reportsTable).where(eq(reportsTable.locationId, locationId)) + + return reports + }, + { + response: t.Array( + t.Object({ + id: t.Number(), + userId: t.Nullable(t.Number()), + createdAt: t.Date(), + locationId: t.String(), + message: t.String() + }) + ) + } ); diff --git a/src/endpoints/schemas.ts b/src/endpoints/schemas.ts index 6ccf14b..b884695 100644 --- a/src/endpoints/schemas.ts +++ b/src/endpoints/schemas.ts @@ -24,6 +24,8 @@ export const LocationSchema = t.Object({ name: t.Nullable(t.String({ examples: ["Schatz"] })), location: t.String(), + ratingsAvg: t.Nullable(t.Number()), + ratingsCount: t.Number(), shortDescription: t.Nullable(t.String()), description: t.String(), url: t.String(), diff --git a/src/env.ts b/src/env.ts index 3aa1829..9f345b6 100644 --- a/src/env.ts +++ b/src/env.ts @@ -21,7 +21,7 @@ const envSchema = z.object({ .transform((x) => x === "true") .default(false), ENV: z.enum(["dev", "staging", "prod"]), - SESSION_COOKIE_SIGNING_SECRET: z.string(), + SESSION_COOKIE_SIGNING_SECRET: z.string().default("lemon melon cookie"), HARDCODE_SESSION_FOR_DEV_TESTING: z.string().optional(), GOOGLE_CLIENT_ID: z.string(), GOOGLE_CLIENT_SECRET: z.string(), diff --git a/tests/database.test.ts b/tests/database.test.ts index dbce728..99e7868 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -34,6 +34,8 @@ const locationIn: ILocation = { const locationOut = { id: "DYNAMICALLY GENERATED, replace with real id", name: "dbTest", + ratingsAvg: null, + ratingsCount: 0, shortDescription: "hi", description: "description", url: "https://hi.com", diff --git a/tests/reviews.test.ts b/tests/reviews.test.ts index 7a596c2..30c240e 100644 --- a/tests/reviews.test.ts +++ b/tests/reviews.test.ts @@ -7,6 +7,7 @@ import { initializeTags, updateTagReview, } from "db/reviews"; +import { QueryUtils } from "db/dbQueryUtils"; import { dbTest } from "./dbstub"; import { createUserSession, DBUser, fetchUserSession } from "db/auth"; import { addLocationDataToDb } from "db/updateLocation"; @@ -380,4 +381,49 @@ describe("location review tests", () => { expect(allReviewsForLocation2).toHaveLength(0); } ); + + reviewTest.concurrent( + "ratingsAvg and ratingsCount test", + async ({ ctx: { db, locationId1, locationId2, user1, user2 } }) => { + const DB = new QueryUtils(db); + const [avgs, counts] = await DB.getRatingsAvgsAndCounts(); + expect(avgs[locationId1]).toBeUndefined(); + expect(counts[locationId1]).toBeUndefined(); + + await addStarReview(db, { + locationId: locationId1, + userId: user1.id, + rating: 3, + }); + await addStarReview(db, { + locationId: locationId1, + userId: user2.id, + rating: 5, + }); + let [avgs2, counts2] = await DB.getRatingsAvgsAndCounts(); + expect(avgs2[locationId1]).toBe(4); + expect(counts2[locationId1]).toBe(2); + + await addStarReview(db, { + locationId: locationId1, + userId: user1.id, + rating: 4, + }); + [avgs2, counts2] = await DB.getRatingsAvgsAndCounts(); + expect(avgs2[locationId1]).toBe(4.5); + expect(counts2[locationId1]).toBe(2); + + await deleteStarReview(db, { locationId: locationId1, userId: user1.id }); + [avgs2, counts2] = await DB.getRatingsAvgsAndCounts(); + expect(avgs2[locationId1]).toBe(5); + expect(counts2[locationId1]).toBe(1); + + await deleteStarReview(db, { locationId: locationId1, userId: user2.id }); + [avgs2, counts2] = await DB.getRatingsAvgsAndCounts(); + expect(avgs2[locationId1]).toBeUndefined(); + expect(counts2[locationId1]).toBeUndefined(); + expect(avgs2[locationId2]).toBeUndefined(); + expect(counts2[locationId2]).toBeUndefined(); + }, + ); });