diff --git a/drizzle/0011_damp_rachel_grey.sql b/drizzle/0011_damp_rachel_grey.sql new file mode 100644 index 0000000..b6fae38 --- /dev/null +++ b/drizzle/0011_damp_rachel_grey.sql @@ -0,0 +1,9 @@ +CREATE TABLE "menu_data" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "menu_data_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "location_id" text NOT NULL, + "images" text[] DEFAULT '{}' NOT NULL, + "menu_items_string" text, + CONSTRAINT "menu_data_location_id_unique" UNIQUE("location_id") +); +--> statement-breakpoint +ALTER TABLE "menu_data" ADD CONSTRAINT "menu_data_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/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..468514d --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1186 @@ +{ + "id": "76a31213-ae57-4db5-bf43-47d3afe83b96", + "prevId": "f2f47465-b027-4494-b217-1e6ca9c4ce8e", + "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.menu_data": { + "name": "menu_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "menu_data_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 + }, + "images": { + "name": "images", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "menu_items_string": { + "name": "menu_items_string", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "menu_data_location_id_location_data_id_fk": { + "name": "menu_data_location_id_location_data_id_fk", + "tableFrom": "menu_data", + "tableTo": "location_data", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "menu_data_location_id_unique": { + "name": "menu_data_location_id_unique", + "nullsNotDistinct": false, + "columns": [ + "location_id" + ] + } + }, + "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 46b2076..1daaa3e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1771716042342, "tag": "0010_shallow_doctor_strange", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1773971830682, + "tag": "0011_damp_rachel_grey", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 325661a..0da16a3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "axios": "^1.10.0", "cheerio": "1.0.0-rc.12", "cookie-signature": "^1.2.2", + "csv-parse": "^6.2.0", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.4", "elysia": "1.4.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2586ef..10de4f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: cookie-signature: specifier: ^1.2.2 version: 1.2.2 + csv-parse: + specifier: ^6.2.0 + version: 6.2.0 drizzle-kit: specifier: ^0.31.4 version: 0.31.4 @@ -1105,56 +1108,67 @@ packages: resolution: {integrity: sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.0': resolution: {integrity: sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.0': resolution: {integrity: sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.0': resolution: {integrity: sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.0': resolution: {integrity: sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.0': resolution: {integrity: sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.0': resolution: {integrity: sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.0': resolution: {integrity: sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.0': resolution: {integrity: sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.0': resolution: {integrity: sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.0': resolution: {integrity: sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.0': resolution: {integrity: sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==} @@ -1597,6 +1611,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-parse@6.2.0: + resolution: {integrity: sha512-Zv8KRHccD1q3BJlK4VcQiEn/+suOOp++89g/fpqOxB2U2tU66uC3yM+ZwU6nQQEJp8AqBIiNqB+pUTKNz4QzKg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -4366,6 +4383,8 @@ snapshots: csstype@3.2.3: optional: true + csv-parse@6.2.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts index f6d932e..090419a 100644 --- a/src/db/getLocations.ts +++ b/src/db/getLocations.ts @@ -3,6 +3,7 @@ import { DateTime } from "luxon"; import { remapAndMergeTimeIntervals } from "utils/timeUtils"; import { ITimeSlot } from "containers/time/parsedTime"; import { DBType } from "./db"; +import { menuDataTable } from "./schema"; /** * @@ -11,92 +12,100 @@ import { DBType } from "./db"; * @returns */ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { - const timeSearchCutoff = today.minus({ days: 1 }); // 1 days worth of data before today + 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 { idToPointOverrides, idToWeeklyOverrides } = await DB.getTimeOverrides( - timeSearchCutoff.toSQLDate(), - ); - const [ratingsAvgs, ratingsCounts] = await DB.getRatingsAvgsAndCounts(); + 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 { 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( - ([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, - timeSearchCutoff, - idToPointOverrides[id], - idToWeeklyOverrides[id], - ), - ), - // .map( - // (time) => - // `${new Date(time.start).toLocaleString()}-${new Date( - // time.end - // ).toLocaleString()}` - // ), - ratingsAvg: ratingsAvgs[id] ?? null, - ratingsCount: ratingsCounts[id] ?? 0, - todaysSoups: specials[id]?.soups ?? [], - todaysSpecials: specials[id]?.specials ?? [], - }; - }, - ); - return finalLocationData; + // fetch menu images and strings + const menuDataList = await db.select().from(menuDataTable); + const menuDataMap = new Map(menuDataList.map((md) => [md.locationId, md])); + + // apply overrides, merge all time intervals, and add specials + const finalLocationData = Object.entries(locationIdToData).map( + ([id, data]) => { + const menuData = menuDataMap.get(id); + 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, + timeSearchCutoff, + idToPointOverrides[id], + idToWeeklyOverrides[id], + ), + ), + // .map( + // (time) => + // `${new Date(time.start).toLocaleString()}-${new Date( + // time.end + // ).toLocaleString()}` + // ), + ratingsAvg: ratingsAvgs[id] ?? null, + ratingsCount: ratingsCounts[id] ?? 0, + todaysSoups: specials[id]?.soups ?? [], + todaysSpecials: specials[id]?.specials ?? [], + images: menuData?.images ?? [], + menuItemsString: menuData?.menuItemsString ?? "", + }; + }, + ); + return finalLocationData; } function applyTimeOverrides( - originalTimes: IDateTimeRange[], - startDate: DateTime, - overridePointTimes?: { [date in string]: ITimeSlot[] }, - overrideWeeklyTimes?: { [weekday in number]: ITimeSlot[] }, + originalTimes: IDateTimeRange[], + startDate: DateTime, + overridePointTimes?: { [date in string]: ITimeSlot[] }, + overrideWeeklyTimes?: { [weekday in number]: ITimeSlot[] }, ) { - if (overridePointTimes === undefined && overrideWeeklyTimes === undefined) - return originalTimes; - if (overrideWeeklyTimes !== undefined) { - for (let i = 0; i < 8; i++) { - // 7 + 1 day prior - const curDate = startDate.plus({ days: i }); - const overrideIntervals = overrideWeeklyTimes[curDate.weekday % 7]; - if (overrideIntervals === undefined) continue; - originalTimes = originalTimes.filter( - (time) => time.date !== curDate.toSQLDate(), - ); - originalTimes.push( - ...overrideIntervals.map((rng) => ({ - date: curDate.toSQLDate(), - startMinutesSinceMidnight: rng.start.hour * 60 + rng.start.minute, - endMinutesSinceMidnight: rng.end.hour * 60 + rng.end.minute, - })), - ); + if (overridePointTimes === undefined && overrideWeeklyTimes === undefined) + return originalTimes; + if (overrideWeeklyTimes !== undefined) { + for (let i = 0; i < 8; i++) { + // 7 + 1 day prior + const curDate = startDate.plus({ days: i }); + const overrideIntervals = overrideWeeklyTimes[curDate.weekday % 7]; + if (overrideIntervals === undefined) continue; + originalTimes = originalTimes.filter( + (time) => time.date !== curDate.toSQLDate(), + ); + originalTimes.push( + ...overrideIntervals.map((rng) => ({ + date: curDate.toSQLDate(), + startMinutesSinceMidnight: + rng.start.hour * 60 + rng.start.minute, + endMinutesSinceMidnight: rng.end.hour * 60 + rng.end.minute, + })), + ); + } } - } - //takes precedence over weekly overrides - if (overridePointTimes !== undefined) { - for (const [overrideDate, timeRanges] of Object.entries( - overridePointTimes, - )) { - originalTimes = originalTimes.filter( - (time) => time.date !== overrideDate, - ); + //takes precedence over weekly overrides + if (overridePointTimes !== undefined) { + for (const [overrideDate, timeRanges] of Object.entries( + overridePointTimes, + )) { + 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, - })), - ); + 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; + return originalTimes; } diff --git a/src/db/schema.ts b/src/db/schema.ts index 1863f01..899e8b9 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -2,251 +2,262 @@ import { sql } from "drizzle-orm"; import { - pgTable, - text, - integer, - boolean, - decimal, - date, - primaryKey, - index, - pgEnum, - timestamp, - uniqueIndex, - check, + pgTable, + text, + integer, + boolean, + decimal, + date, + primaryKey, + index, + pgEnum, + timestamp, + uniqueIndex, + check, } from "drizzle-orm/pg-core"; export const emailTable = pgTable("emails", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - name: text("name").notNull(), - email: text("email").notNull(), + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + name: text("name").notNull(), + email: text("email").notNull(), }); export const externalIdType = pgEnum("externalIdType", ["concept_id"]); export const externalIdToInternalIdTable = pgTable( - "external_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(), - type: externalIdType("external_id_type").default("concept_id"), - }, - (table) => [index("internal_id").on(table.internalId)], + "external_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(), + type: externalIdType("external_id_type").default("concept_id"), + }, + (table) => [index("internal_id").on(table.internalId)], ); 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(), + 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)], + "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", { - 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("coordinate_lat", { mode: "number", scale: 30 }), - coordinateLng: decimal("coordinate_lng", { mode: "number", scale: 30 }), - acceptsOnlineOrders: boolean("accepts_online_orders"), + 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("coordinate_lat", { mode: "number", scale: 30 }), + coordinateLng: decimal("coordinate_lng", { mode: "number", scale: 30 }), + acceptsOnlineOrders: boolean("accepts_online_orders"), }); 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] })], + "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 weeklyTimeOverwritesTable = pgTable( - "weekly_time_overwrites_table", - { - locationId: text("location_id") - .notNull() - .references(() => locationDataTable.id, { - onDelete: "cascade", - }), - /** sunday is 0, monday is 1, ... */ - weekday: integer("weekday").notNull(), - timeString: text("time_string").notNull(), - }, - (t) => [ - primaryKey({ columns: [t.locationId, t.weekday] }), - check("weekday_check", sql`${t.weekday} >= 0 AND ${t.weekday} < 7`), // sunday is 0, monday is 1, etc. - ], + "weekly_time_overwrites_table", + { + locationId: text("location_id") + .notNull() + .references(() => locationDataTable.id, { + onDelete: "cascade", + }), + /** sunday is 0, monday is 1, ... */ + weekday: integer("weekday").notNull(), + timeString: text("time_string").notNull(), + }, + (t) => [ + primaryKey({ columns: [t.locationId, t.weekday] }), + check("weekday_check", sql`${t.weekday} >= 0 AND ${t.weekday} < 7`), // sunday is 0, monday is 1, etc. + ], ); 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(), + 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(), }); export const userTable = pgTable( - "users", - { - id: integer("id").notNull().generatedByDefaultAsIdentity().primaryKey(), - googleId: text("google_id").notNull(), - email: text("email").notNull(), - firstName: text("first_name"), - lastName: text("last_name"), - pictureUrl: text("picture_url"), - createdAt: timestamp("created_at", { - withTimezone: true, - mode: "date", - }).defaultNow(), - updatedAt: timestamp("updated_at", { - withTimezone: true, - mode: "date", - }).defaultNow(), - }, - (table) => [index("google_id").on(table.googleId)], + "users", + { + id: integer("id").notNull().generatedByDefaultAsIdentity().primaryKey(), + googleId: text("google_id").notNull(), + email: text("email").notNull(), + firstName: text("first_name"), + lastName: text("last_name"), + pictureUrl: text("picture_url"), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "date", + }).defaultNow(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "date", + }).defaultNow(), + }, + (table) => [index("google_id").on(table.googleId)], ); export const userSessionTable = pgTable("sessions", { - sessionId: text("id").notNull().primaryKey(), - userId: integer("user_id") - .references(() => userTable.id, { - onDelete: "cascade", - }) - .notNull(), - expiresAt: timestamp("expires_at", { - withTimezone: true, - mode: "date", - }).notNull(), - createdAt: timestamp("created_at", { - withTimezone: true, - mode: "date", - }).defaultNow(), + sessionId: text("id").notNull().primaryKey(), + userId: integer("user_id") + .references(() => userTable.id, { + onDelete: "cascade", + }) + .notNull(), + expiresAt: timestamp("expires_at", { + withTimezone: true, + mode: "date", + }).notNull(), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "date", + }).defaultNow(), }); export const tagListTable = pgTable("tag_list", { - id: integer("id").generatedAlwaysAsIdentity().primaryKey(), - name: text("name").notNull(), - sortOrder: integer("sort_order").notNull(), + id: integer("id").generatedAlwaysAsIdentity().primaryKey(), + name: text("name").notNull(), + sortOrder: integer("sort_order").notNull(), }); export const tagReviewTable = pgTable( - "tag_reviews", - { - id: integer("id").generatedAlwaysAsIdentity().primaryKey(), - tagId: integer("tag_id") - .notNull() - .references(() => tagListTable.id, { onDelete: "cascade" }), - userId: integer("user_id") - .notNull() - .references(() => userTable.id, { onDelete: "cascade" }), - locationId: text("location_id") - .notNull() - .references(() => locationDataTable.id, { onDelete: "cascade" }), - vote: boolean("vote").notNull(), - writtenReview: text("written_review"), // nullable by default - hidden: boolean("hidden").default(false), // moderation purposes - createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) - .notNull() - .defaultNow(), - }, - (t) => [ - uniqueIndex("tag_reviews_location_tag_user_uniq").on( - t.locationId, - t.tagId, - t.userId, - ), - ], + "tag_reviews", + { + id: integer("id").generatedAlwaysAsIdentity().primaryKey(), + tagId: integer("tag_id") + .notNull() + .references(() => tagListTable.id, { onDelete: "cascade" }), + userId: integer("user_id") + .notNull() + .references(() => userTable.id, { onDelete: "cascade" }), + locationId: text("location_id") + .notNull() + .references(() => locationDataTable.id, { onDelete: "cascade" }), + vote: boolean("vote").notNull(), + writtenReview: text("written_review"), // nullable by default + hidden: boolean("hidden").default(false), // moderation purposes + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .notNull() + .defaultNow(), + }, + (t) => [ + uniqueIndex("tag_reviews_location_tag_user_uniq").on( + t.locationId, + t.tagId, + t.userId, + ), + ], ); export const starReviewTable = pgTable( - "star_reviews", - { - id: integer("id").generatedAlwaysAsIdentity().primaryKey(), - userId: integer("user_id") - .notNull() - .references(() => userTable.id, { onDelete: "cascade" }), - locationId: text("location_id") - .notNull() - .references(() => locationDataTable.id, { onDelete: "cascade" }), - starRating: decimal("star_rating", { - precision: 2, - scale: 1, - mode: "number", - }).notNull(), - createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) - .notNull() - .defaultNow(), - }, - (t) => [ - uniqueIndex("star_reviews_location_user_uniq").on(t.locationId, t.userId), - check( - "rating_number_check", - sql`${t.starRating} > 0 AND ${t.starRating} <= 5 AND mod(${t.starRating}*2,1) = 0`, - ), // rating is a multiple of .5 - ], + "star_reviews", + { + id: integer("id").generatedAlwaysAsIdentity().primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => userTable.id, { onDelete: "cascade" }), + locationId: text("location_id") + .notNull() + .references(() => locationDataTable.id, { onDelete: "cascade" }), + starRating: decimal("star_rating", { + precision: 2, + scale: 1, + mode: "number", + }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) + .notNull() + .defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .notNull() + .defaultNow(), + }, + (t) => [ + uniqueIndex("star_reviews_location_user_uniq").on( + t.locationId, + t.userId, + ), + check( + "rating_number_check", + sql`${t.starRating} > 0 AND ${t.starRating} <= 5 AND mod(${t.starRating}*2,1) = 0`, + ), // 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", { +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(), - } -) + }) + .notNull() + .defaultNow(), + locationId: text("location_id") + .references(() => locationDataTable.id, { + onDelete: "cascade", + }) + .notNull(), + message: text("message").notNull(), +}); + +export const menuDataTable = pgTable("menu_data", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + locationId: text("location_id") + .references(() => locationDataTable.id, { onDelete: "cascade" }) + .notNull() + .unique(), + images: text("images").array().notNull().default([]), + menuItemsString: text("menu_items_string"), +}); diff --git a/src/endpoints/misc.ts b/src/endpoints/misc.ts index 9590e1f..de45216 100644 --- a/src/endpoints/misc.ts +++ b/src/endpoints/misc.ts @@ -7,12 +7,43 @@ import { notifySlack } from "utils/slack"; import { LocationsSchema } from "./schemas"; import { env } from "env"; import { eq } from "drizzle-orm" +import { readFile } from "node:fs/promises"; +import path from "node:path"; import { locationDataTable, reportsTable } from "db/schema"; import { fetchUserDetails } from "./auth"; import { sendEmail } from "utils/email"; +const menuImagesDir = path.resolve(process.cwd(), "public", "menu_images"); + +function withTrailingSlash(value: string) { + return value.endsWith("/") ? value : `${value}/`; +} + +function normalizeImagePath(imageValue: string) { + const trimmedValue = imageValue.trim().replace(/^\/+/, ""); + return trimmedValue.startsWith("menu_images/") + ? trimmedValue + : `menu_images/${trimmedValue}`; +} + +function toAbsoluteImageUrl(requestUrl: string, imageValue: string) { + if (/^https?:\/\//i.test(imageValue)) return imageValue; + const normalizedPath = normalizeImagePath(imageValue) + .split("/") + .filter(Boolean) + .map((segment) => encodeURIComponent(segment)) + .join("/"); + const baseUrl = env.MENU_IMAGE_BASE_URL?.trim(); + + if (baseUrl) { + return new URL(normalizedPath, withTrailingSlash(baseUrl)).toString(); + } + + return new URL(normalizedPath, withTrailingSlash(new URL(requestUrl).origin)).toString(); +} + export const miscEndpoints = new Elysia(); miscEndpoints.get( "/", @@ -23,7 +54,15 @@ miscEndpoints.get( ); miscEndpoints.get( "/v2/locations", - async () => await getAllLocationsFromDB(db, DateTime.now()), + async ({ request }) => { + const locations = await getAllLocationsFromDB(db, DateTime.now()); + return locations.map((location) => ({ + ...location, + images: location.images.map((image) => + toAbsoluteImageUrl(request.url, image), + ), + })); + }, { response: LocationsSchema, detail: { @@ -32,6 +71,29 @@ miscEndpoints.get( }, }, ); +miscEndpoints.get("/menu_images/:filename", async ({ params: { filename } }) => { + const normalizedFilename = filename.replace(/^\/+/, ""); + const filePath = path.resolve(menuImagesDir, normalizedFilename); + if (!filePath.startsWith(menuImagesDir)) { + throw new Response("Invalid filename", { status: 400 }); + } + + try { + const file = await readFile(filePath); + const extension = path.extname(normalizedFilename).toLowerCase(); + const contentType = + extension === ".jpg" || extension === ".jpeg" + ? "image/jpeg" + : extension === ".webp" + ? "image/webp" + : "image/png"; + return new Response(file, { + headers: { "Content-Type": contentType }, + }); + } catch { + throw new Response("Not found", { status: 404 }); + } +}); miscEndpoints.get("/emails", async () => await new QueryUtils(db).getEmails(), { response: t.Array( t.Object({ diff --git a/src/endpoints/schemas.ts b/src/endpoints/schemas.ts index b884695..b939357 100644 --- a/src/endpoints/schemas.ts +++ b/src/endpoints/schemas.ts @@ -36,6 +36,8 @@ export const LocationSchema = t.Object({ id: t.String(), conceptId: t.Nullable(t.String()), + images: t.Array(t.String()), + menuItemsString: t.Nullable(t.String()), }); export const LocationsSchema = t.Array(LocationSchema); diff --git a/src/env.ts b/src/env.ts index 9f345b6..6bd0bbd 100644 --- a/src/env.ts +++ b/src/env.ts @@ -13,6 +13,7 @@ const envSchema = z.object({ IN_TEST_MODE: z.stringbool().default(false), SLACK_MESSAGE_PREFIX: z.string().default("local-dev"), DATABASE_URL: z.string(), + MENU_IMAGE_BASE_URL: z.string().optional(), OIDC_SERVER: z.string().transform((x) => new URL(x)), OIDC_CLIENT_ID: z.string(), OIDC_CLIENT_SECRET: z.string(),