diff --git a/apps/cli/src/commands/data.ts b/apps/cli/src/commands/data.ts new file mode 100644 index 000000000..940bb8c52 --- /dev/null +++ b/apps/cli/src/commands/data.ts @@ -0,0 +1,264 @@ +import program from "@peated/cli/program"; +import { db } from "@peated/server/db"; +import { tags } from "@peated/server/db/schema"; +import type { FlavorProfile, TagCategory } from "@peated/server/types"; + +const TAGS: Record = { + cereal: [ + "barley sugar", + "biscuity", + "oatmeal", + "grains", + "malted barley", + "rye", + "wheat", + "corn", + "fresh hay", + "bread", + ], + fruity: [ + "acidic", + "green apple", + "lemon zest", + "fresh berries", + "citrus", + "lime", + "ripe peach", + "juicy pear", + "mellow apricot", + "soft cherry", + "ripe melon", + "banana", + "apple pie", + "stewed fruits", + ], + floral: [ + "light floral", + "soft floral", + "lavender", + "fresh flowers", + "green leaves", + "heather", + "delicate herbs", + ], + peaty: [ + "alcohol burn", + "light smoke", + "gentle peat", + "smoked herbs", + "peat moss", + "earthy peat", + "smoked wood", + "balanced smoke", + "intense smoke", + "heavy iodine", + "tar", + "burnt rubber", + "creosote", + "smoked meat", + "charred oak", + "deep peat", + ], + feinty: [ + "old books", + "antique leather", + "tobacco", + "earthiness", + "soft musk", + "aged sherry", + "library books", + "tobacco leaf", + ], + sulphury: ["burnt matches", "gunpowder"], + woody: [ + "anise", + "baking spices", + "nutmeg", + "old oak", + "polished wood", + "vanilla bean", + "creamy oak", + "maple syrup", + "butterscotch", + "ripe apples", + "toasted almonds", + "oak spices", + "juicy pears", + "antique wood", + "oak", + "sherry", + ], + winey: [ + "luxurious sherry", + "dark chocolate", + "walnut", + "coffee beans", + "date", + "currants", + "rich prunes", + "dried figs", + "raisins", + "fruit cake", + ], +}; + +const PROFILES: Record = { + young_spritely: [ + "green apple", + "lemon zest", + "fresh berries", + "citrus", + "grass", + "lime", + "light floral", + "sparkling", + ], + sweet_fruit_mellow: [ + "cake", + "candy", + "ripe peach", + "juicy pear", + "mellow apricot", + "soft cherry", + "barley sugar", + "marmalade", + "ripe melon", + "banana", + ], + spicy_sweet: [ + "anise", + "baking spices", + "cinnamon", + "sweet ginger", + "vanilla", + "caramel", + "honey", + "rich nutmeg", + "warm clove", + "spiced cake", + ], + spicy_dry: [ + "bitter", + "fiery", + "dry pepper", + "herbal notes", + "crisp ginger", + "subtle oak", + "cardamom", + "sandalwood", + "black tea", + "tobacco leaf", + ], + deep_rich_dried_fruit: [ + "dried figs", + "raisins", + "rich prunes", + "luxurious sherry", + "dark chocolate", + "walnut", + "coffee beans", + "date", + ], + old_dignified: [ + "balanced", + "antique leather", + "old oak", + "tobacco", + "polished wood", + "earthiness", + "soft musk", + "aged sherry", + "library books", + ], + light_delicate: [ + "acidic", + "clean", + "soft floral", + "lavender", + "green leaves", + "light honey", + "almond", + "heather", + "fresh hay", + "delicate herbs", + ], + juicy_oak_vanilla: [ + "butter", + "vanilla bean", + "creamy oak", + "maple syrup", + "butterscotch", + "ripe apples", + "toasted almonds", + "oak spices", + "juicy pears", + ], + oily_coastal: [ + "ethanol", + "seaweed", + "oily texture", + "salt spray", + "maritime air", + "briny notes", + "clam", + "oyster", + "fishing net", + ], + lightly_peated: [ + "ethanol", + "light smoke", + "gentle peat", + "fresh mineral", + "smoked herbs", + "bonfire embers", + "peat moss", + "autumn leaves", + "grilled citrus", + ], + peated: [ + "earthy peat", + "smoked wood", + "balanced smoke", + "leather", + "campfire", + "black pepper", + "wet stones", + "forest floor", + ], + heavily_peated: [ + "alcohol burn", + "fiery", + "intense smoke", + "heavy iodine", + "tar", + "burnt rubber", + "creosote", + "smoked meat", + "charred oak", + "deep peat", + ], +}; + +const subcommand = program.command("data"); + +subcommand.command("load-default-tags").action(async (options) => { + Object.entries(TAGS).forEach(async ([categoryName, tagList]) => { + await db.transaction(async (tx) => { + for (const tagName of tagList) { + console.log(`Registering tag ${tagName}`); + await tx + .insert(tags) + .values({ + name: tagName, + tagCategory: categoryName as TagCategory, + flavorProfiles: Object.entries(PROFILES) + .filter(([profileName, tagList]) => tagList.includes(tagName)) + .map( + ([profileName]) => profileName as FlavorProfile, + ), + }) + .onConflictDoNothing(); + } + }); + }); +}); diff --git a/apps/cli/src/commands/index.ts b/apps/cli/src/commands/index.ts index ad61b00a4..e6bebe1d0 100644 --- a/apps/cli/src/commands/index.ts +++ b/apps/cli/src/commands/index.ts @@ -1,4 +1,5 @@ export * from "./bottles"; +export * from "./data"; export * from "./db"; export * from "./entities"; export * from "./mocks"; diff --git a/apps/server/migrations/0092_parched_kat_farrell.sql b/apps/server/migrations/0092_parched_kat_farrell.sql new file mode 100644 index 000000000..9306a07a1 --- /dev/null +++ b/apps/server/migrations/0092_parched_kat_farrell.sql @@ -0,0 +1,43 @@ +DO $$ BEGIN + CREATE TYPE "category" AS ENUM('blend', 'bourbon', 'rye', 'single_grain', 'single_malt', 'single_pot_still', 'spirit'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "flavor_profile" AS ENUM('young_spritely', 'sweet_fruit_mellow', 'spicy_sweet', 'spicy_dry', 'deep_rich_dried_fruit', 'old_dignified', 'light_delicate', 'juicy_oak_vanilla', 'oily_coastal', 'lightly_peated', 'peated', 'heavily_peated'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "object_type" AS ENUM('bottle', 'comment', 'entity', 'tasting', 'toast', 'follow'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +DO $$ BEGIN + CREATE TYPE "tag_category" AS ENUM('cereal', 'fruity', 'floral', 'peaty', 'feinty', 'sulphury', 'woody', 'winey'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +CREATE TABLE IF NOT EXISTS "bottle_flavor_profile" ( + "bottle_id" bigint NOT NULL, + "flavor_profile" "flavor_profile" NOT NULL, + "count" integer DEFAULT 0 NOT NULL, + CONSTRAINT "bottle_flavor_profile_bottle_id_flavor_profile_pk" PRIMARY KEY("bottle_id","flavor_profile") +); + +CREATE TABLE IF NOT EXISTS "tag" ( + "name" varchar(64) PRIMARY KEY NOT NULL, + "synonyms" varchar(64)[] DEFAULT '{}' NOT NULL, + "tag_category" "tag_category" NOT NULL, + "flavor_profile" flavor_profile[] DEFAULT '{}' NOT NULL +); + +DO $$ BEGIN + ALTER TABLE "bottle_flavor_profile" ADD CONSTRAINT "bottle_flavor_profile_bottle_id_bottle_id_fk" FOREIGN KEY ("bottle_id") REFERENCES "bottle"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/server/migrations/0093_robust_beast.sql b/apps/server/migrations/0093_robust_beast.sql new file mode 100644 index 000000000..2ba15696d --- /dev/null +++ b/apps/server/migrations/0093_robust_beast.sql @@ -0,0 +1 @@ +ALTER TABLE "tasting" ADD COLUMN "color" integer; \ No newline at end of file diff --git a/apps/server/migrations/meta/0092_snapshot.json b/apps/server/migrations/meta/0092_snapshot.json new file mode 100644 index 000000000..4f1c91864 --- /dev/null +++ b/apps/server/migrations/meta/0092_snapshot.json @@ -0,0 +1,2315 @@ +{ + "id": "c656d71f-910c-4f70-851f-b8d7fcb05098", + "prevId": "1b8174dc-30aa-456d-85cb-24b04505c59b", + "version": "5", + "dialect": "pg", + "tables": { + "badge_award": { + "name": "badge_award", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "badge_id": { + "name": "badge_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "smallint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "level": { + "name": "level", + "type": "smallint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "badge_award_unq": { + "name": "badge_award_unq", + "columns": [ + "badge_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "badge_award_badge_id_badges_id_fk": { + "name": "badge_award_badge_id_badges_id_fk", + "tableFrom": "badge_award", + "tableTo": "badges", + "columnsFrom": [ + "badge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "badge_award_user_id_user_id_fk": { + "name": "badge_award_user_id_user_id_fk", + "tableFrom": "badge_award", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "badges": { + "name": "badges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "badge_type", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "badge_name_unq": { + "name": "badge_name_unq", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle_alias": { + "name": "bottle_alias", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bottle_alias_bottle_idx": { + "name": "bottle_alias_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bottle_alias_bottle_id_bottle_id_fk": { + "name": "bottle_alias_bottle_id_bottle_id_fk", + "tableFrom": "bottle_alias", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_alias_name_pk": { + "name": "bottle_alias_name_pk", + "columns": [ + "name" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_flavor_profile": { + "name": "bottle_flavor_profile", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_flavor_profile_bottle_id_bottle_id_fk": { + "name": "bottle_flavor_profile_bottle_id_bottle_id_fk", + "tableFrom": "bottle_flavor_profile", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_flavor_profile_bottle_id_flavor_profile_pk": { + "name": "bottle_flavor_profile_bottle_id_flavor_profile_pk", + "columns": [ + "bottle_id", + "flavor_profile" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_tag": { + "name": "bottle_tag", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_tag_bottle_id_bottle_id_fk": { + "name": "bottle_tag_bottle_id_bottle_id_fk", + "tableFrom": "bottle_tag", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_tag_bottle_id_tag_pk": { + "name": "bottle_tag_bottle_id_tag_pk", + "columns": [ + "bottle_id", + "tag" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_tombstone": { + "name": "bottle_tombstone", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "new_bottle_id": { + "name": "new_bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle": { + "name": "bottle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "category", + "primaryKey": false, + "notNull": false + }, + "brand_id": { + "name": "brand_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottler_id": { + "name": "bottler_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stated_age": { + "name": "stated_age", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tasting_notes": { + "name": "tasting_notes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suggested_tags": { + "name": "suggested_tags", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::varchar[]" + }, + "avg_rating": { + "name": "avg_rating", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_tastings": { + "name": "total_tastings", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bottle_brand_unq": { + "name": "bottle_brand_unq", + "columns": [ + "name", + "brand_id" + ], + "isUnique": true + }, + "bottle_full_name_unq": { + "name": "bottle_full_name_unq", + "columns": [ + "full_name" + ], + "isUnique": true + }, + "bottle_brand_idx": { + "name": "bottle_brand_idx", + "columns": [ + "brand_id" + ], + "isUnique": false + }, + "bottle_bottler_idx": { + "name": "bottle_bottler_idx", + "columns": [ + "bottler_id" + ], + "isUnique": false + }, + "bottle_created_by_idx": { + "name": "bottle_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + }, + "bottle_category_idx": { + "name": "bottle_category_idx", + "columns": [ + "category" + ], + "isUnique": false + }, + "bottle_flavor_profile_idx": { + "name": "bottle_flavor_profile_idx", + "columns": [ + "flavor_profile" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bottle_brand_id_entity_id_fk": { + "name": "bottle_brand_id_entity_id_fk", + "tableFrom": "bottle", + "tableTo": "entity", + "columnsFrom": [ + "brand_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_bottler_id_entity_id_fk": { + "name": "bottle_bottler_id_entity_id_fk", + "tableFrom": "bottle", + "tableTo": "entity", + "columnsFrom": [ + "bottler_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_created_by_id_user_id_fk": { + "name": "bottle_created_by_id_user_id_fk", + "tableFrom": "bottle", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle_distiller": { + "name": "bottle_distiller", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "distiller_id": { + "name": "distiller_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_distiller_bottle_id_bottle_id_fk": { + "name": "bottle_distiller_bottle_id_bottle_id_fk", + "tableFrom": "bottle_distiller", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_distiller_distiller_id_entity_id_fk": { + "name": "bottle_distiller_distiller_id_entity_id_fk", + "tableFrom": "bottle_distiller", + "tableTo": "entity", + "columnsFrom": [ + "distiller_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_distiller_bottle_id_distiller_id_pk": { + "name": "bottle_distiller_bottle_id_distiller_id_pk", + "columns": [ + "bottle_id", + "distiller_id" + ] + } + }, + "uniqueConstraints": {} + }, + "change": { + "name": "change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "object_type": { + "name": "object_type", + "type": "object_type", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "primaryKey": false, + "notNull": true, + "default": "'add'" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "change_created_by_idx": { + "name": "change_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_created_by_id_user_id_fk": { + "name": "change_created_by_id_user_id_fk", + "tableFrom": "change", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "collection_bottle": { + "name": "collection_bottle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collection_bottle_unq": { + "name": "collection_bottle_unq", + "columns": [ + "collection_id", + "bottle_id" + ], + "isUnique": true + }, + "collection_bottle_bottle_idx": { + "name": "collection_bottle_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collection_bottle_collection_id_collection_id_fk": { + "name": "collection_bottle_collection_id_collection_id_fk", + "tableFrom": "collection_bottle", + "tableTo": "collection", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "collection_bottle_bottle_id_bottle_id_fk": { + "name": "collection_bottle_bottle_id_bottle_id_fk", + "tableFrom": "collection_bottle", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "collection": { + "name": "collection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "total_bottles": { + "name": "total_bottles", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "collection_name_unq": { + "name": "collection_name_unq", + "columns": [ + "name", + "created_by_id" + ], + "isUnique": true + }, + "collection_created_by_idx": { + "name": "collection_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collection_created_by_id_user_id_fk": { + "name": "collection_created_by_id_user_id_fk", + "tableFrom": "collection", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "tasting_id": { + "name": "tasting_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_unq": { + "name": "comment_unq", + "columns": [ + "tasting_id", + "created_by_id", + "created_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comments_tasting_id_tasting_id_fk": { + "name": "comments_tasting_id_tasting_id_fk", + "tableFrom": "comments", + "tableTo": "tasting", + "columnsFrom": [ + "tasting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_created_by_id_user_id_fk": { + "name": "comments_created_by_id_user_id_fk", + "tableFrom": "comments", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "entity": { + "name": "entity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "entity_type[]", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year_established": { + "name": "year_established", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "geography", + "primaryKey": false, + "notNull": false + }, + "total_bottles": { + "name": "total_bottles", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tastings": { + "name": "total_tastings", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_name_unq": { + "name": "entity_name_unq", + "columns": [ + "name" + ], + "isUnique": true + }, + "entity_created_by_idx": { + "name": "entity_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "entity_created_by_id_user_id_fk": { + "name": "entity_created_by_id_user_id_fk", + "tableFrom": "entity", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "entity_tombstone": { + "name": "entity_tombstone", + "schema": "", + "columns": { + "entity_id": { + "name": "entity_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "new_entity_id": { + "name": "new_entity_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "external_site_config": { + "name": "external_site_config", + "schema": "", + "columns": { + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "external_site_config_external_site_id_external_site_id_fk": { + "name": "external_site_config_external_site_id_external_site_id_fk", + "tableFrom": "external_site_config", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_site_config_external_site_id_key_pk": { + "name": "external_site_config_external_site_id_key_pk", + "columns": [ + "external_site_id", + "key" + ] + } + }, + "uniqueConstraints": {} + }, + "external_site": { + "name": "external_site", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "external_site_type", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_every": { + "name": "run_every", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_site_type": { + "name": "external_site_type", + "columns": [ + "type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "flight_bottle": { + "name": "flight_bottle", + "schema": "", + "columns": { + "flight_id": { + "name": "flight_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "flight_bottle_flight_id_flight_id_fk": { + "name": "flight_bottle_flight_id_flight_id_fk", + "tableFrom": "flight_bottle", + "tableTo": "flight", + "columnsFrom": [ + "flight_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "flight_bottle_bottle_id_bottle_id_fk": { + "name": "flight_bottle_bottle_id_bottle_id_fk", + "tableFrom": "flight_bottle", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "flight_bottle_flight_id_bottle_id_pk": { + "name": "flight_bottle_flight_id_bottle_id_pk", + "columns": [ + "flight_id", + "bottle_id" + ] + } + }, + "uniqueConstraints": {} + }, + "flight": { + "name": "flight", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "flight_public_id": { + "name": "flight_public_id", + "columns": [ + "public_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "flight_created_by_id_user_id_fk": { + "name": "flight_created_by_id_user_id_fk", + "tableFrom": "flight", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "follow": { + "name": "follow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "from_user_id": { + "name": "from_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "to_user_id": { + "name": "to_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "follow_status", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follow_unq": { + "name": "follow_unq", + "columns": [ + "from_user_id", + "to_user_id" + ], + "isUnique": true + }, + "follow_to_user_idx": { + "name": "follow_to_user_idx", + "columns": [ + "to_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "follow_from_user_id_user_id_fk": { + "name": "follow_from_user_id_user_id_fk", + "tableFrom": "follow", + "tableTo": "user", + "columnsFrom": [ + "from_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "follow_to_user_id_user_id_fk": { + "name": "follow_to_user_id_user_id_fk", + "tableFrom": "follow", + "tableTo": "user", + "columnsFrom": [ + "to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "identity": { + "name": "identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "identity_provider", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "identity_unq": { + "name": "identity_unq", + "columns": [ + "provider", + "external_id" + ], + "isUnique": true + }, + "identity_user_idx": { + "name": "identity_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identity_user_id_user_id_fk": { + "name": "identity_user_id_user_id_fk", + "tableFrom": "identity", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "from_user_id": { + "name": "from_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "notifications_unq": { + "name": "notifications_unq", + "columns": [ + "user_id", + "object_id", + "type", + "created_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_from_user_id_user_id_fk": { + "name": "notifications_from_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "from_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue": { + "name": "issue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_unq_name": { + "name": "review_unq_name", + "columns": [ + "external_site_id", + "name", + "issue" + ], + "isUnique": true + }, + "review_bottle_idx": { + "name": "review_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_external_site_id_external_site_id_fk": { + "name": "review_external_site_id_external_site_id_fk", + "tableFrom": "review", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_bottle_id_bottle_id_fk": { + "name": "review_bottle_id_bottle_id_fk", + "tableFrom": "review", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_url_unique": { + "name": "review_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "store_price_history": { + "name": "store_price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "volume": { + "name": "volume", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_price_history_unq": { + "name": "store_price_history_unq", + "columns": [ + "price_id", + "volume", + "date" + ], + "isUnique": true + } + }, + "foreignKeys": { + "store_price_history_price_id_store_price_id_fk": { + "name": "store_price_history_price_id_store_price_id_fk", + "tableFrom": "store_price_history", + "tableTo": "store_price", + "columnsFrom": [ + "price_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "store_price": { + "name": "store_price", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "volume": { + "name": "volume", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_price_unq_name": { + "name": "store_price_unq_name", + "columns": [ + "external_site_id", + "name", + "volume" + ], + "isUnique": true + }, + "store_price_bottle_idx": { + "name": "store_price_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "store_price_external_site_id_external_site_id_fk": { + "name": "store_price_external_site_id_external_site_id_fk", + "tableFrom": "store_price", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "store_price_bottle_id_bottle_id_fk": { + "name": "store_price_bottle_id_bottle_id_fk", + "tableFrom": "store_price", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "store_price_url_unique": { + "name": "store_price_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "tag": { + "name": "tag", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "synonyms": { + "name": "synonyms", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "tag_category": { + "name": "tag_category", + "type": "tag_category", + "primaryKey": false, + "notNull": true + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tasting": { + "name": "tasting", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::varchar[]" + }, + "rating": { + "name": "rating", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serving_style": { + "name": "serving_style", + "type": "servingStyle", + "primaryKey": false, + "notNull": false + }, + "friends": { + "name": "friends", + "type": "bigint[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::bigint[]" + }, + "flight_id": { + "name": "flight_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "toasts": { + "name": "toasts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tasting_unq": { + "name": "tasting_unq", + "columns": [ + "bottle_id", + "created_by_id", + "created_at" + ], + "isUnique": true + }, + "tasting_bottle_idx": { + "name": "tasting_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + }, + "tasting_flight_idx": { + "name": "tasting_flight_idx", + "columns": [ + "flight_id" + ], + "isUnique": false + }, + "tasting_created_by_idx": { + "name": "tasting_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasting_bottle_id_bottle_id_fk": { + "name": "tasting_bottle_id_bottle_id_fk", + "tableFrom": "tasting", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasting_flight_id_flight_id_fk": { + "name": "tasting_flight_id_flight_id_fk", + "tableFrom": "tasting", + "tableTo": "flight", + "columnsFrom": [ + "flight_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasting_created_by_id_user_id_fk": { + "name": "tasting_created_by_id_user_id_fk", + "tableFrom": "tasting", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "toasts": { + "name": "toasts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "tasting_id": { + "name": "tasting_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "toast_unq": { + "name": "toast_unq", + "columns": [ + "tasting_id", + "created_by_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "toasts_tasting_id_tasting_id_fk": { + "name": "toasts_tasting_id_tasting_id_fk", + "tableFrom": "toasts", + "tableTo": "tasting", + "columnsFrom": [ + "tasting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "toasts_created_by_id_user_id_fk": { + "name": "toasts_created_by_id_user_id_fk", + "tableFrom": "toasts", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture_url": { + "name": "picture_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "admin": { + "name": "admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mod": { + "name": "mod", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_email_unq": { + "name": "user_email_unq", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unq": { + "name": "user_username_unq", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "badge_type": { + "name": "badge_type", + "values": { + "bottle": "bottle", + "region": "region", + "category": "category" + } + }, + "type": { + "name": "type", + "values": { + "add": "add", + "update": "update", + "delete": "delete" + } + }, + "entity_type": { + "name": "entity_type", + "values": { + "brand": "brand", + "distiller": "distiller", + "bottler": "bottler" + } + }, + "category": { + "name": "category", + "values": { + "blend": "blend", + "bourbon": "bourbon", + "rye": "rye", + "single_grain": "single_grain", + "single_malt": "single_malt", + "single_pot_still": "single_pot_still", + "spirit": "spirit" + } + }, + "flavor_profile": { + "name": "flavor_profile", + "values": { + "young_spritely": "young_spritely", + "sweet_fruit_mellow": "sweet_fruit_mellow", + "spicy_sweet": "spicy_sweet", + "spicy_dry": "spicy_dry", + "deep_rich_dried_fruit": "deep_rich_dried_fruit", + "old_dignified": "old_dignified", + "light_delicate": "light_delicate", + "juicy_oak_vanilla": "juicy_oak_vanilla", + "oily_coastal": "oily_coastal", + "lightly_peated": "lightly_peated", + "peated": "peated", + "heavily_peated": "heavily_peated" + } + }, + "object_type": { + "name": "object_type", + "values": { + "bottle": "bottle", + "comment": "comment", + "entity": "entity", + "tasting": "tasting", + "toast": "toast", + "follow": "follow" + } + }, + "tag_category": { + "name": "tag_category", + "values": { + "cereal": "cereal", + "fruity": "fruity", + "floral": "floral", + "peaty": "peaty", + "feinty": "feinty", + "sulphury": "sulphury", + "woody": "woody", + "winey": "winey" + } + }, + "external_site_type": { + "name": "external_site_type", + "values": { + "astorwines": "astorwines", + "healthyspirits": "healthyspirits", + "smws": "smws", + "smwsa": "smwsa", + "totalwine": "totalwine", + "woodencork": "woodencork", + "whiskyadvocate": "whiskyadvocate" + } + }, + "follow_status": { + "name": "follow_status", + "values": { + "none": "none", + "pending": "pending", + "following": "following" + } + }, + "identity_provider": { + "name": "identity_provider", + "values": { + "google": "google" + } + }, + "notification_type": { + "name": "notification_type", + "values": { + "comment": "comment", + "toast": "toast", + "friend_request": "friend_request" + } + }, + "servingStyle": { + "name": "servingStyle", + "values": { + "neat": "neat", + "rocks": "rocks", + "splash": "splash" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/migrations/meta/0093_snapshot.json b/apps/server/migrations/meta/0093_snapshot.json new file mode 100644 index 000000000..e2f454d42 --- /dev/null +++ b/apps/server/migrations/meta/0093_snapshot.json @@ -0,0 +1,2321 @@ +{ + "id": "ad11d3b9-6889-4dc2-834f-62a04328bcf3", + "prevId": "c656d71f-910c-4f70-851f-b8d7fcb05098", + "version": "5", + "dialect": "pg", + "tables": { + "badge_award": { + "name": "badge_award", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "badge_id": { + "name": "badge_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "smallint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "level": { + "name": "level", + "type": "smallint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "badge_award_unq": { + "name": "badge_award_unq", + "columns": [ + "badge_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "badge_award_badge_id_badges_id_fk": { + "name": "badge_award_badge_id_badges_id_fk", + "tableFrom": "badge_award", + "tableTo": "badges", + "columnsFrom": [ + "badge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "badge_award_user_id_user_id_fk": { + "name": "badge_award_user_id_user_id_fk", + "tableFrom": "badge_award", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "badges": { + "name": "badges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "badge_type", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "badge_name_unq": { + "name": "badge_name_unq", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle_alias": { + "name": "bottle_alias", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bottle_alias_bottle_idx": { + "name": "bottle_alias_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bottle_alias_bottle_id_bottle_id_fk": { + "name": "bottle_alias_bottle_id_bottle_id_fk", + "tableFrom": "bottle_alias", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_alias_name_pk": { + "name": "bottle_alias_name_pk", + "columns": [ + "name" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_flavor_profile": { + "name": "bottle_flavor_profile", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_flavor_profile_bottle_id_bottle_id_fk": { + "name": "bottle_flavor_profile_bottle_id_bottle_id_fk", + "tableFrom": "bottle_flavor_profile", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_flavor_profile_bottle_id_flavor_profile_pk": { + "name": "bottle_flavor_profile_bottle_id_flavor_profile_pk", + "columns": [ + "bottle_id", + "flavor_profile" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_tag": { + "name": "bottle_tag", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_tag_bottle_id_bottle_id_fk": { + "name": "bottle_tag_bottle_id_bottle_id_fk", + "tableFrom": "bottle_tag", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_tag_bottle_id_tag_pk": { + "name": "bottle_tag_bottle_id_tag_pk", + "columns": [ + "bottle_id", + "tag" + ] + } + }, + "uniqueConstraints": {} + }, + "bottle_tombstone": { + "name": "bottle_tombstone", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "new_bottle_id": { + "name": "new_bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle": { + "name": "bottle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "category", + "primaryKey": false, + "notNull": false + }, + "brand_id": { + "name": "brand_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottler_id": { + "name": "bottler_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stated_age": { + "name": "stated_age", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tasting_notes": { + "name": "tasting_notes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suggested_tags": { + "name": "suggested_tags", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::varchar[]" + }, + "avg_rating": { + "name": "avg_rating", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_tastings": { + "name": "total_tastings", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bottle_brand_unq": { + "name": "bottle_brand_unq", + "columns": [ + "name", + "brand_id" + ], + "isUnique": true + }, + "bottle_full_name_unq": { + "name": "bottle_full_name_unq", + "columns": [ + "full_name" + ], + "isUnique": true + }, + "bottle_brand_idx": { + "name": "bottle_brand_idx", + "columns": [ + "brand_id" + ], + "isUnique": false + }, + "bottle_bottler_idx": { + "name": "bottle_bottler_idx", + "columns": [ + "bottler_id" + ], + "isUnique": false + }, + "bottle_created_by_idx": { + "name": "bottle_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + }, + "bottle_category_idx": { + "name": "bottle_category_idx", + "columns": [ + "category" + ], + "isUnique": false + }, + "bottle_flavor_profile_idx": { + "name": "bottle_flavor_profile_idx", + "columns": [ + "flavor_profile" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bottle_brand_id_entity_id_fk": { + "name": "bottle_brand_id_entity_id_fk", + "tableFrom": "bottle", + "tableTo": "entity", + "columnsFrom": [ + "brand_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_bottler_id_entity_id_fk": { + "name": "bottle_bottler_id_entity_id_fk", + "tableFrom": "bottle", + "tableTo": "entity", + "columnsFrom": [ + "bottler_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_created_by_id_user_id_fk": { + "name": "bottle_created_by_id_user_id_fk", + "tableFrom": "bottle", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bottle_distiller": { + "name": "bottle_distiller", + "schema": "", + "columns": { + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "distiller_id": { + "name": "distiller_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bottle_distiller_bottle_id_bottle_id_fk": { + "name": "bottle_distiller_bottle_id_bottle_id_fk", + "tableFrom": "bottle_distiller", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bottle_distiller_distiller_id_entity_id_fk": { + "name": "bottle_distiller_distiller_id_entity_id_fk", + "tableFrom": "bottle_distiller", + "tableTo": "entity", + "columnsFrom": [ + "distiller_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bottle_distiller_bottle_id_distiller_id_pk": { + "name": "bottle_distiller_bottle_id_distiller_id_pk", + "columns": [ + "bottle_id", + "distiller_id" + ] + } + }, + "uniqueConstraints": {} + }, + "change": { + "name": "change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "object_type": { + "name": "object_type", + "type": "object_type", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "primaryKey": false, + "notNull": true, + "default": "'add'" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "change_created_by_idx": { + "name": "change_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_created_by_id_user_id_fk": { + "name": "change_created_by_id_user_id_fk", + "tableFrom": "change", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "collection_bottle": { + "name": "collection_bottle", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collection_bottle_unq": { + "name": "collection_bottle_unq", + "columns": [ + "collection_id", + "bottle_id" + ], + "isUnique": true + }, + "collection_bottle_bottle_idx": { + "name": "collection_bottle_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collection_bottle_collection_id_collection_id_fk": { + "name": "collection_bottle_collection_id_collection_id_fk", + "tableFrom": "collection_bottle", + "tableTo": "collection", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "collection_bottle_bottle_id_bottle_id_fk": { + "name": "collection_bottle_bottle_id_bottle_id_fk", + "tableFrom": "collection_bottle", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "collection": { + "name": "collection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "total_bottles": { + "name": "total_bottles", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "collection_name_unq": { + "name": "collection_name_unq", + "columns": [ + "name", + "created_by_id" + ], + "isUnique": true + }, + "collection_created_by_idx": { + "name": "collection_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "collection_created_by_id_user_id_fk": { + "name": "collection_created_by_id_user_id_fk", + "tableFrom": "collection", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "tasting_id": { + "name": "tasting_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_unq": { + "name": "comment_unq", + "columns": [ + "tasting_id", + "created_by_id", + "created_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "comments_tasting_id_tasting_id_fk": { + "name": "comments_tasting_id_tasting_id_fk", + "tableFrom": "comments", + "tableTo": "tasting", + "columnsFrom": [ + "tasting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comments_created_by_id_user_id_fk": { + "name": "comments_created_by_id_user_id_fk", + "tableFrom": "comments", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "entity": { + "name": "entity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "entity_type[]", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year_established": { + "name": "year_established", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "geography", + "primaryKey": false, + "notNull": false + }, + "total_bottles": { + "name": "total_bottles", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tastings": { + "name": "total_tastings", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_name_unq": { + "name": "entity_name_unq", + "columns": [ + "name" + ], + "isUnique": true + }, + "entity_created_by_idx": { + "name": "entity_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "entity_created_by_id_user_id_fk": { + "name": "entity_created_by_id_user_id_fk", + "tableFrom": "entity", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "entity_tombstone": { + "name": "entity_tombstone", + "schema": "", + "columns": { + "entity_id": { + "name": "entity_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "new_entity_id": { + "name": "new_entity_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "external_site_config": { + "name": "external_site_config", + "schema": "", + "columns": { + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "external_site_config_external_site_id_external_site_id_fk": { + "name": "external_site_config_external_site_id_external_site_id_fk", + "tableFrom": "external_site_config", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "external_site_config_external_site_id_key_pk": { + "name": "external_site_config_external_site_id_key_pk", + "columns": [ + "external_site_id", + "key" + ] + } + }, + "uniqueConstraints": {} + }, + "external_site": { + "name": "external_site", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "external_site_type", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_every": { + "name": "run_every", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "external_site_type": { + "name": "external_site_type", + "columns": [ + "type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "flight_bottle": { + "name": "flight_bottle", + "schema": "", + "columns": { + "flight_id": { + "name": "flight_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "flight_bottle_flight_id_flight_id_fk": { + "name": "flight_bottle_flight_id_flight_id_fk", + "tableFrom": "flight_bottle", + "tableTo": "flight", + "columnsFrom": [ + "flight_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "flight_bottle_bottle_id_bottle_id_fk": { + "name": "flight_bottle_bottle_id_bottle_id_fk", + "tableFrom": "flight_bottle", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "flight_bottle_flight_id_bottle_id_pk": { + "name": "flight_bottle_flight_id_bottle_id_pk", + "columns": [ + "flight_id", + "bottle_id" + ] + } + }, + "uniqueConstraints": {} + }, + "flight": { + "name": "flight", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "flight_public_id": { + "name": "flight_public_id", + "columns": [ + "public_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "flight_created_by_id_user_id_fk": { + "name": "flight_created_by_id_user_id_fk", + "tableFrom": "flight", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "follow": { + "name": "follow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "from_user_id": { + "name": "from_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "to_user_id": { + "name": "to_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "follow_status", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follow_unq": { + "name": "follow_unq", + "columns": [ + "from_user_id", + "to_user_id" + ], + "isUnique": true + }, + "follow_to_user_idx": { + "name": "follow_to_user_idx", + "columns": [ + "to_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "follow_from_user_id_user_id_fk": { + "name": "follow_from_user_id_user_id_fk", + "tableFrom": "follow", + "tableTo": "user", + "columnsFrom": [ + "from_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "follow_to_user_id_user_id_fk": { + "name": "follow_to_user_id_user_id_fk", + "tableFrom": "follow", + "tableTo": "user", + "columnsFrom": [ + "to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "identity": { + "name": "identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "identity_provider", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "identity_unq": { + "name": "identity_unq", + "columns": [ + "provider", + "external_id" + ], + "isUnique": true + }, + "identity_user_idx": { + "name": "identity_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "identity_user_id_user_id_fk": { + "name": "identity_user_id_user_id_fk", + "tableFrom": "identity", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "from_user_id": { + "name": "from_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "notifications_unq": { + "name": "notifications_unq", + "columns": [ + "user_id", + "object_id", + "type", + "created_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_from_user_id_user_id_fk": { + "name": "notifications_from_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "from_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue": { + "name": "issue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_unq_name": { + "name": "review_unq_name", + "columns": [ + "external_site_id", + "name", + "issue" + ], + "isUnique": true + }, + "review_bottle_idx": { + "name": "review_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_external_site_id_external_site_id_fk": { + "name": "review_external_site_id_external_site_id_fk", + "tableFrom": "review", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_bottle_id_bottle_id_fk": { + "name": "review_bottle_id_bottle_id_fk", + "tableFrom": "review", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_url_unique": { + "name": "review_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "store_price_history": { + "name": "store_price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "volume": { + "name": "volume", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_price_history_unq": { + "name": "store_price_history_unq", + "columns": [ + "price_id", + "volume", + "date" + ], + "isUnique": true + } + }, + "foreignKeys": { + "store_price_history_price_id_store_price_id_fk": { + "name": "store_price_history_price_id_store_price_id_fk", + "tableFrom": "store_price_history", + "tableTo": "store_price", + "columnsFrom": [ + "price_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "store_price": { + "name": "store_price", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "external_site_id": { + "name": "external_site_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "volume": { + "name": "volume", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "store_price_unq_name": { + "name": "store_price_unq_name", + "columns": [ + "external_site_id", + "name", + "volume" + ], + "isUnique": true + }, + "store_price_bottle_idx": { + "name": "store_price_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "store_price_external_site_id_external_site_id_fk": { + "name": "store_price_external_site_id_external_site_id_fk", + "tableFrom": "store_price", + "tableTo": "external_site", + "columnsFrom": [ + "external_site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "store_price_bottle_id_bottle_id_fk": { + "name": "store_price_bottle_id_bottle_id_fk", + "tableFrom": "store_price", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "store_price_url_unique": { + "name": "store_price_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "tag": { + "name": "tag", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "synonyms": { + "name": "synonyms", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "tag_category": { + "name": "tag_category", + "type": "tag_category", + "primaryKey": false, + "notNull": true + }, + "flavor_profile": { + "name": "flavor_profile", + "type": "flavor_profile[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tasting": { + "name": "tasting", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "bottle_id": { + "name": "bottle_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "varchar(64)[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::varchar[]" + }, + "color": { + "name": "color", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serving_style": { + "name": "serving_style", + "type": "servingStyle", + "primaryKey": false, + "notNull": false + }, + "friends": { + "name": "friends", + "type": "bigint[]", + "primaryKey": false, + "notNull": true, + "default": "array[]::bigint[]" + }, + "flight_id": { + "name": "flight_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "toasts": { + "name": "toasts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tasting_unq": { + "name": "tasting_unq", + "columns": [ + "bottle_id", + "created_by_id", + "created_at" + ], + "isUnique": true + }, + "tasting_bottle_idx": { + "name": "tasting_bottle_idx", + "columns": [ + "bottle_id" + ], + "isUnique": false + }, + "tasting_flight_idx": { + "name": "tasting_flight_idx", + "columns": [ + "flight_id" + ], + "isUnique": false + }, + "tasting_created_by_idx": { + "name": "tasting_created_by_idx", + "columns": [ + "created_by_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasting_bottle_id_bottle_id_fk": { + "name": "tasting_bottle_id_bottle_id_fk", + "tableFrom": "tasting", + "tableTo": "bottle", + "columnsFrom": [ + "bottle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasting_flight_id_flight_id_fk": { + "name": "tasting_flight_id_flight_id_fk", + "tableFrom": "tasting", + "tableTo": "flight", + "columnsFrom": [ + "flight_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasting_created_by_id_user_id_fk": { + "name": "tasting_created_by_id_user_id_fk", + "tableFrom": "tasting", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "toasts": { + "name": "toasts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "tasting_id": { + "name": "tasting_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_id": { + "name": "created_by_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "toast_unq": { + "name": "toast_unq", + "columns": [ + "tasting_id", + "created_by_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "toasts_tasting_id_tasting_id_fk": { + "name": "toasts_tasting_id_tasting_id_fk", + "tableFrom": "toasts", + "tableTo": "tasting", + "columnsFrom": [ + "tasting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "toasts_created_by_id_user_id_fk": { + "name": "toasts_created_by_id_user_id_fk", + "tableFrom": "toasts", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "picture_url": { + "name": "picture_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "admin": { + "name": "admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mod": { + "name": "mod", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_email_unq": { + "name": "user_email_unq", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unq": { + "name": "user_username_unq", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "badge_type": { + "name": "badge_type", + "values": { + "bottle": "bottle", + "region": "region", + "category": "category" + } + }, + "type": { + "name": "type", + "values": { + "add": "add", + "update": "update", + "delete": "delete" + } + }, + "entity_type": { + "name": "entity_type", + "values": { + "brand": "brand", + "distiller": "distiller", + "bottler": "bottler" + } + }, + "category": { + "name": "category", + "values": { + "blend": "blend", + "bourbon": "bourbon", + "rye": "rye", + "single_grain": "single_grain", + "single_malt": "single_malt", + "single_pot_still": "single_pot_still", + "spirit": "spirit" + } + }, + "flavor_profile": { + "name": "flavor_profile", + "values": { + "young_spritely": "young_spritely", + "sweet_fruit_mellow": "sweet_fruit_mellow", + "spicy_sweet": "spicy_sweet", + "spicy_dry": "spicy_dry", + "deep_rich_dried_fruit": "deep_rich_dried_fruit", + "old_dignified": "old_dignified", + "light_delicate": "light_delicate", + "juicy_oak_vanilla": "juicy_oak_vanilla", + "oily_coastal": "oily_coastal", + "lightly_peated": "lightly_peated", + "peated": "peated", + "heavily_peated": "heavily_peated" + } + }, + "object_type": { + "name": "object_type", + "values": { + "bottle": "bottle", + "comment": "comment", + "entity": "entity", + "tasting": "tasting", + "toast": "toast", + "follow": "follow" + } + }, + "tag_category": { + "name": "tag_category", + "values": { + "cereal": "cereal", + "fruity": "fruity", + "floral": "floral", + "peaty": "peaty", + "feinty": "feinty", + "sulphury": "sulphury", + "woody": "woody", + "winey": "winey" + } + }, + "external_site_type": { + "name": "external_site_type", + "values": { + "astorwines": "astorwines", + "healthyspirits": "healthyspirits", + "smws": "smws", + "smwsa": "smwsa", + "totalwine": "totalwine", + "woodencork": "woodencork", + "whiskyadvocate": "whiskyadvocate" + } + }, + "follow_status": { + "name": "follow_status", + "values": { + "none": "none", + "pending": "pending", + "following": "following" + } + }, + "identity_provider": { + "name": "identity_provider", + "values": { + "google": "google" + } + }, + "notification_type": { + "name": "notification_type", + "values": { + "comment": "comment", + "toast": "toast", + "friend_request": "friend_request" + } + }, + "servingStyle": { + "name": "servingStyle", + "values": { + "neat": "neat", + "rocks": "rocks", + "splash": "splash" + } + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/migrations/meta/_journal.json b/apps/server/migrations/meta/_journal.json index 664bae9c8..90c11a3d0 100644 --- a/apps/server/migrations/meta/_journal.json +++ b/apps/server/migrations/meta/_journal.json @@ -645,6 +645,20 @@ "when": 1712726602484, "tag": "0091_late_namora", "breakpoints": false + }, + { + "idx": 92, + "version": "5", + "when": 1713109664250, + "tag": "0092_parched_kat_farrell", + "breakpoints": false + }, + { + "idx": 93, + "version": "5", + "when": 1713125101461, + "tag": "0093_robust_beast", + "breakpoints": false } ] } \ No newline at end of file diff --git a/apps/server/src/constants.ts b/apps/server/src/constants.ts index 20096e3c3..8db69a5ee 100644 --- a/apps/server/src/constants.ts +++ b/apps/server/src/constants.ts @@ -4,21 +4,6 @@ export const MAX_FILESIZE = 1048576 * 20; export const XP_PER_LEVEL = 5; -export const FLAVOR_PROFILES = [ - "young_spritely", - "sweet_fruit_mellow", - "spicy_sweet", - "spicy_dry", - "deep_rich_dried_fruit", - "old_dignified", - "light_delicate", - "juicy_oak_vanilla", - "oily_coastal", - "lightly_peated", - "peated", - "heavily_peated", -] as const; - export const COUNTRY_LIST = [ "Afghanistan", "Albania", @@ -308,192 +293,66 @@ export const BADGE_TYPE_LIST = ["bottle", "region", "category"] as const; // https://whiskeytrends.com/whiskey-tasting-terminology/ // https://www.bonigala.com/25-ways-to-describe-whisky -export const DEFAULT_TAGS = [ - "acidic", - "alcohol burn", - "almond", - "anise", - "apple", - "astringent", - "balanced", - "baking spices", - "bananas", - "berry", - "biscuits", - "bitter", - "blackberry", - "black cherry", - "black pepper", - "bland", - "bread", - "brine", - "burnt", - "butter", - "butterscotch", - "cake", - "campfire", - "candied cherries", - "candy", - "caramel", - "cask", - "cedar", + +export const FLAVOR_PROFILES = [ + "young_spritely", + "sweet_fruit_mellow", + "spicy_sweet", + "spicy_dry", + "deep_rich_dried_fruit", + "old_dignified", + "light_delicate", + "juicy_oak_vanilla", + "oily_coastal", + "lightly_peated", + "peated", + "heavily_peated", +] as const; + +// TODO: maybe utilize https://www.whiskymax.co.uk/charles-macleans-whisky-wheel/ +// instead? its a bit easier to reason about for +export const TAG_CATEGORIES = [ "cereal", - "charcoal", - "charred", - "cherry", - "chocolate", - "cigar box", - "cinnamon", - "citrus", - "clean", - "clover", - "cloves", - "cocoa", - "coconut", - "coffee", - "complex", - "cookies", - "course", - "craisins", - "cranberry", - "creamy", - "creosote", - "currant", - "cut grass", - "dates", - "dried apricots", - "dried fruit", - "dry", - "dry", - "earth", - "esters", - "ethanol", - "eucalyptus", - "fatty", - "fen", - "fiery", - "figs", - "fireplace", - "flat", - "flowery", - "fragrant", - "fresh", - "fresh fruit", - "fruit cake", "fruity", - "full-bodied", - "grain", - "grassy", - "green", - "green apple", - "hard", - "harsh", - "hay", - "heath", - "heather", - "heavy", - "herbal", - "honey", - "hot", - "iodine", - "kelp", - "kippers", - "lasting", - "laurel", - "leafy", - "leather", - "lemon", - "light", - "lime", - "liqueur", - "long", - "malted milk", - "malty", - "maple", - "marshmallow", - "meadow", - "medicinal", - "mellow", - "melon", - "mint", - "molasses", - "moor", - "moss", - "musty", - "neutral", - "nuanced", - "nutmeg", - "nutty", - "oak", - "oatmeal", - "ocean", - "oily", - "oloroso", - "orange", - "peach", - "pear", - "peat", - "perfume", - "phenolic", - "pine", - "pipe tobacco", - "plum", - "port", - "prunes", - "raisins", - "red wine", - "resin", - "rich", - "robust", - "rope", - "rose petals", - "rosewater", - "round", - "rum", - "rye", - "salt", - "sap", - "savory", - "seaside", - "seaweed", - "sharp", - "sherry", - "short", - "shortbread", - "silage", - "smoke", - "smooth", - "soft", - "solvent", - "spiced", - "spicy", - "spirity", - "subtle", - "sulphuric", - "sweet", - "sweet", - "syrup", - "tannic", - "tar", - "tarry rope", - "tart", - "tea", - "thin", - "toast", - "tobacco", - "toffee", - "turpentine", - "vanilla", - "vegetal", - "violets", - "walnuts", - "warmth", - "wet dog", - "wine", + "floral", + "peaty", + "feinty", + "sulphury", "woody", - "zesty", + "winey", ] as const; +// TODO: reference whisky magazine for numerical, but simplify +export const COLOR_SCALE = [ + [0, "Clear", "#ffffff"], + [1, "White Wine", "#fffbe0"], + [2, "Melon Yellow", "#fdeda2"], + [3, "Fine Sherry", "#faea8a"], + [4, "Pale Honey", "#f7e07a"], + [5, "Pale Gold", "#f5db6d"], + [6, "Medium Gold", "#f5d863"], + [7, "Deep Gold", "#f0ce62"], + [8, "Amontillado Sherry", "#f0c962"], + [9, "Pale Brown", "#efc358"], + [10, "Medium Brown", "#efbf50"], + [11, "Deep Brown", "#e0ae3d"], + [12, "Palo Coratdo Sherry", "#dea03d"], + [13, "Burn Amber", "#da9635"], + [14, "Copper", "#cf7831"], + [15, "Tawny", "#d06c3a"], + [16, "Deep Tawhny", "#bf573a"], + [17, "Oloroso Sherry", "#a23a2f"], + [18, "Vintage Oak", "#932e24"], + [19, "Moscatel Sherry", "#6a3022"], + [20, "Black Bowmore", "#3b1d12"], +] as const; + +// TODO: +export const CASK_FILLS = ["1st_fill", "2nd_fill", "refill"]; + +// TODO: determine granularity (e.g. hogshead etc) +export const CASK_TYPES = ["bourbon", "sherry", "other"] as const; + // used for web scraping export const defaultHeaders = (url: string) => { const urlParts = new URL(url); diff --git a/apps/server/src/db/schema/bottles.ts b/apps/server/src/db/schema/bottles.ts index 214d2e037..eb05c655b 100644 --- a/apps/server/src/db/schema/bottles.ts +++ b/apps/server/src/db/schema/bottles.ts @@ -204,3 +204,32 @@ export const bottleTombstonesRelations = relations( export type BottleTombstone = typeof bottleTombstones.$inferSelect; export type NewBottleTombstone = typeof bottleTombstones.$inferInsert; + +export const bottleFlavorProfiles = pgTable( + "bottle_flavor_profile", + { + bottleId: bigint("bottle_id", { mode: "number" }) + .references(() => bottles.id) + .notNull(), + flavorProfile: flavorProfileEnum("flavor_profile").notNull(), + count: integer("count").default(0).notNull(), + }, + (table) => { + return { + pk: primaryKey(table.bottleId, table.flavorProfile), + }; + }, +); + +export const bottleFlavorProfilesRelations = relations( + bottleFlavorProfiles, + ({ one }) => ({ + bottle: one(bottles, { + fields: [bottleFlavorProfiles.bottleId], + references: [bottles.id], + }), + }), +); + +export type BottleFlavorProfile = typeof bottleFlavorProfiles.$inferSelect; +export type NewBottleFlavorProfile = typeof bottleFlavorProfiles.$inferInsert; diff --git a/apps/server/src/db/schema/enums.ts b/apps/server/src/db/schema/enums.ts index aad7ce2db..1b1bfde1a 100644 --- a/apps/server/src/db/schema/enums.ts +++ b/apps/server/src/db/schema/enums.ts @@ -1,6 +1,10 @@ import { pgEnum } from "drizzle-orm/pg-core"; -import { CATEGORY_LIST, FLAVOR_PROFILES } from "../../constants"; +import { + CATEGORY_LIST, + FLAVOR_PROFILES, + TAG_CATEGORIES, +} from "../../constants"; export const categoryEnum = pgEnum("category", CATEGORY_LIST); @@ -14,3 +18,5 @@ export const objectTypeEnum = pgEnum("object_type", [ ]); export const flavorProfileEnum = pgEnum("flavor_profile", FLAVOR_PROFILES); + +export const tagCategoryEnum = pgEnum("tag_category", TAG_CATEGORIES); diff --git a/apps/server/src/db/schema/index.ts b/apps/server/src/db/schema/index.ts index 50510c0dc..46192a6c2 100644 --- a/apps/server/src/db/schema/index.ts +++ b/apps/server/src/db/schema/index.ts @@ -4,6 +4,7 @@ export * from "./changes"; export * from "./collections"; export * from "./comments"; export * from "./entities"; +export * from "./enums"; export * from "./externalSites"; export * from "./flights"; export * from "./follows"; @@ -11,6 +12,7 @@ export * from "./identities"; export * from "./notifications"; export * from "./reviews"; export * from "./stores"; +export * from "./tags"; export * from "./tastings"; export * from "./toasts"; export * from "./users"; diff --git a/apps/server/src/db/schema/tags.ts b/apps/server/src/db/schema/tags.ts new file mode 100644 index 000000000..43c38cbad --- /dev/null +++ b/apps/server/src/db/schema/tags.ts @@ -0,0 +1,19 @@ +import { sql } from "drizzle-orm"; +import { pgTable, varchar } from "drizzle-orm/pg-core"; +import { flavorProfileEnum, tagCategoryEnum } from "./enums"; + +export const tags = pgTable("tag", { + name: varchar("name", { length: 64 }).notNull().primaryKey(), + synonyms: varchar("synonyms", { length: 64 }) + .array() + .default(sql`'{}'`) + .notNull(), + tagCategory: tagCategoryEnum("tag_category").notNull(), + flavorProfiles: flavorProfileEnum("flavor_profile") + .array() + .default(sql`'{}'`) + .notNull(), +}); + +export type Tag = typeof tags.$inferSelect; +export type NewTag = typeof tags.$inferInsert; diff --git a/apps/server/src/db/schema/tastings.ts b/apps/server/src/db/schema/tastings.ts index 20829b0a9..d3b075d1c 100644 --- a/apps/server/src/db/schema/tastings.ts +++ b/apps/server/src/db/schema/tastings.ts @@ -32,6 +32,7 @@ export const tastings = pgTable( .array() .default(sql`array[]::varchar[]`) .notNull(), + color: integer("color"), rating: doublePrecision("rating"), imageUrl: text("image_url"), notes: text("notes"), diff --git a/apps/server/src/jobs/generateBottleDetails.ts b/apps/server/src/jobs/generateBottleDetails.ts index 522faab9b..6c4ae5b14 100644 --- a/apps/server/src/jobs/generateBottleDetails.ts +++ b/apps/server/src/jobs/generateBottleDetails.ts @@ -2,7 +2,6 @@ import config from "@peated/server/config"; import { CATEGORY_LIST, DEFAULT_CREATED_BY_ID, - DEFAULT_TAGS, FLAVOR_PROFILES, } from "@peated/server/constants"; import { db } from "@peated/server/db"; @@ -20,7 +19,7 @@ if (!config.OPENAI_API_KEY) { console.warn("OPENAI_API_KEY is not configured."); } -function generatePrompt(bottle: Bottle) { +function generatePrompt(bottle: Bottle, tagList: string[]) { const infoLines = []; if (bottle.category) { infoLines.push(`Category: ${bottle.category}`); @@ -59,13 +58,10 @@ If the whiskey is made in Scotland, it is always spelled "whisky". 'suggestedTags' should be up to five items that reflect the flavor of this spirit the best. Values MUST be from the following list: -- ${DEFAULT_TAGS.join("\n- ")} +- ${tagList.join("\n- ")} `; } -// XXX: enums dont work with GPT currently (it ignores them) -const DefaultTagEnum = z.enum(DEFAULT_TAGS); - const OpenAIBottleDetailsSchema = z.object({ description: z.string().nullish(), tastingNotes: z @@ -90,12 +86,15 @@ const OpenAIBottleDetailsValidationSchema = OpenAIBottleDetailsSchema.extend({ type Response = z.infer; -async function generateBottleDetails(bottle: Bottle): Promise { +async function generateBottleDetails( + bottle: Bottle, + tagList: string[], +): Promise { if (!config.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY is not configured"); const result = await getStructuredResponse( - generatePrompt(bottle), + generatePrompt(bottle, tagList), OpenAIBottleDetailsSchema, OpenAIBottleDetailsValidationSchema, undefined, @@ -117,7 +116,9 @@ export default async function ({ bottleId }: { bottleId: number }) { if (!bottle) { throw new Error(`Unknown bottle: ${bottleId}`); } - const result = await generateBottleDetails(bottle); + + const tagList = (await db.query.tags.findMany()).map((r) => r.name); + const result = await generateBottleDetails(bottle, tagList); if (!result) { throw new Error(`Failed to generate details for bottle: ${bottleId}`); @@ -138,16 +139,12 @@ export default async function ({ bottleId }: { bottleId: number }) { result.suggestedTags?.length && !arraysEqual(result.suggestedTags, bottle.suggestedTags) ) { - if ( - !result.suggestedTags.find((t) => !DefaultTagEnum.safeParse(t).success) - ) { + if (!result.suggestedTags.find((t) => !tagList.includes(t))) { data.suggestedTags = result.suggestedTags; } else { logError(`Invalid value for suggestedTags`, { tag: { - values: result.suggestedTags.filter( - (t) => !DefaultTagEnum.safeParse(t).success, - ), + values: result.suggestedTags.filter((t) => !tagList.includes(t)), }, bottle: { id: bottle.id, diff --git a/apps/server/src/lib/format.ts b/apps/server/src/lib/format.ts index ca97e8161..428493411 100644 --- a/apps/server/src/lib/format.ts +++ b/apps/server/src/lib/format.ts @@ -1,5 +1,10 @@ import { toTitleCase } from "@peated/server/lib/strings"; -import type { Category, FlavorProfile } from "@peated/server/types"; +import type { + Category, + FlavorProfile, + ServingStyle, +} from "@peated/server/types"; +import { COLOR_SCALE } from "../constants"; export function formatCategoryName( value: Category | string | undefined | null, @@ -72,3 +77,17 @@ export function notesForProfile(profile: FlavorProfile): string { return ""; } } + +export function formatServingStyle(style: ServingStyle) { + return toTitleCase(style); +} + +export function formatColor(colorValue: number) { + let value = ""; + for (const [threshold, color] of COLOR_SCALE) { + if (colorValue >= threshold) { + value = color; + } + } + return value; +} diff --git a/apps/server/src/lib/rand.ts b/apps/server/src/lib/rand.ts index 3eacddf73..b65fd275d 100644 --- a/apps/server/src/lib/rand.ts +++ b/apps/server/src/lib/rand.ts @@ -2,7 +2,7 @@ export function random(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); } -export function choose(choices: T[]): T { +export function choose(choices: T[] | readonly T[]): T { const index = Math.floor(Math.random() * choices.length); return choices[index]; } diff --git a/apps/server/src/lib/test/fixtures.ts b/apps/server/src/lib/test/fixtures.ts index 0720ac2a7..3e944299e 100644 --- a/apps/server/src/lib/test/fixtures.ts +++ b/apps/server/src/lib/test/fixtures.ts @@ -1,5 +1,5 @@ import { faker } from "@faker-js/faker"; -import type * as dbSchema from "@peated/server/db/schema"; +import * as dbSchema from "@peated/server/db/schema"; import { generatePublicId } from "@peated/server/lib/publicId"; import { type ExternalSiteType } from "@peated/server/types"; import { eq, sql } from "drizzle-orm"; @@ -7,8 +7,9 @@ import { readFile } from "fs/promises"; import path from "path"; import { CATEGORY_LIST, - DEFAULT_TAGS, EXTERNAL_SITE_TYPE_LIST, + FLAVOR_PROFILES, + TAG_CATEGORIES, } from "../../constants"; import type { DatabaseType } from "../../db"; import { db as dbConn } from "../../db"; @@ -109,7 +110,7 @@ export const Entity = async ( { ...data }: Partial> = {}, db: DatabaseType = dbConn, ): Promise => { - const name = faker.company.name(); + const name = data.name || faker.company.name(); // XXX(dcramer): not ideal const existing = await db.query.entities.findFirst({ where: (entities, { eq }) => eq(entities.name, name), @@ -257,12 +258,16 @@ export const Tasting = async ( db: DatabaseType = dbConn, ): Promise => { return await db.transaction(async (tx) => { + const tags = []; + for (let i = 0; i <= random(1, 5); i++) { + tags.push((await Tag({}, tx)).name); + } const [result] = await tx .insert(tastings) .values({ notes: faker.lorem.sentence(), rating: faker.number.float({ min: 1, max: 5 }), - tags: sample(DEFAULT_TAGS, random(1, 5)), + tags: tags, ...data, bottleId: data.bottleId || (await Bottle({}, tx)).id, createdById: data.createdById || (await User({}, tx)).id, @@ -388,7 +393,7 @@ export const ExternalSite = async ( { ...data }: Partial> = {}, db: DatabaseType = dbConn, ): Promise => { - if (!data.type) data.type = choose([...EXTERNAL_SITE_TYPE_LIST]); + if (!data.type) data.type = choose(EXTERNAL_SITE_TYPE_LIST); // XXX(dcramer): not ideal const existing = await db.query.externalSites.findFirst({ where: (externalSites, { eq }) => @@ -540,6 +545,34 @@ export const Collection = async ( return result; }; +export const Tag = async ( + { ...data }: Partial> = {}, + db: DatabaseType = dbConn, +): Promise => { + const name = data.name || faker.word.adjective().toLowerCase(); + + // XXX(dcramer): not ideal + const existing = await db.query.tags.findFirst({ + where: (tags, { eq }) => eq(tags.name, name), + }); + if (existing) return existing; + + const [result] = await db + .insert(dbSchema.tags) + .values({ + name, + tagCategory: choose(TAG_CATEGORIES), + flavorProfiles: sample(FLAVOR_PROFILES, random(1, 2)), + ...(data as Omit< + dbSchema.NewTag, + "name" | "tagCategory" | "flavorProfiles" + >), + }) + .returning(); + if (!result) throw new Error("Unable to create fixture"); + return result; +}; + export const AuthToken = async ( { user }: { user?: dbSchema.User | null } = {}, db: DatabaseType = dbConn, diff --git a/apps/server/src/schemas/comments.ts b/apps/server/src/schemas/comments.ts index a9d5b3a17..3e2b4f403 100644 --- a/apps/server/src/schemas/comments.ts +++ b/apps/server/src/schemas/comments.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { zDatetime } from "./common"; import { UserSchema } from "./users"; export const CommentSchema = z.object({ @@ -10,5 +11,5 @@ export const CommentSchema = z.object({ export const CommentInputSchema = z.object({ comment: z.string().trim().min(1, "Required"), - createdAt: z.string().datetime(), + createdAt: zDatetime, }); diff --git a/apps/server/src/schemas/common.ts b/apps/server/src/schemas/common.ts new file mode 100644 index 000000000..eabb5f059 --- /dev/null +++ b/apps/server/src/schemas/common.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { isDistantFuture, isDistantPast } from "../lib/dates"; + +export const zTag = z.string(); + +export const zDatetime = z + .string() + .datetime() + .superRefine((value, ctx) => { + const newValue = new Date(value); + if (isDistantFuture(newValue, 60 * 5)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Value too far in future.", + }); + + return z.NEVER; + } + + if (isDistantPast(newValue, 60 * 60 * 24 * 7)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Value too far in the past.", + }); + + return z.NEVER; + } + + return value; + }); diff --git a/apps/server/src/schemas/tastings.ts b/apps/server/src/schemas/tastings.ts index 59d455c8c..28606c25b 100644 --- a/apps/server/src/schemas/tastings.ts +++ b/apps/server/src/schemas/tastings.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SERVING_STYLE_LIST } from "../constants"; import { BottleSchema } from "./bottles"; +import { zDatetime, zTag } from "./common"; import { UserSchema } from "./users"; export const ServiceStyleEnum = z.enum(SERVING_STYLE_LIST); @@ -12,6 +13,7 @@ export const TastingSchema = z.object({ bottle: BottleSchema, rating: z.number().gte(0).lte(5).nullable(), tags: z.array(z.string()), + color: z.number().gte(0).lte(20).nullable(), servingStyle: ServiceStyleEnum.nullable(), friends: z.array(UserSchema), @@ -24,22 +26,24 @@ export const TastingSchema = z.object({ export const TastingInputSchema = z.object({ bottle: z.number(), - notes: z.string().nullable().optional(), - rating: z.number().gte(0).lte(5).nullable().optional(), - tags: z.array(z.string()).max(15).nullable().optional(), + notes: z.string().nullish(), + rating: z.number().gte(0).lte(5).nullish(), + tags: z.array(zTag).max(15).nullish(), + color: z.number().gte(0).lte(20).nullish(), - servingStyle: ServiceStyleEnum.nullable().optional(), + servingStyle: ServiceStyleEnum.nullish(), friends: z.array(z.number()).optional(), - flight: z.string().nullable().optional(), + flight: z.string().nullish(), - createdAt: z.string().datetime().optional(), + createdAt: zDatetime.optional(), }); export const TastingUpdateSchema = z.object({ - notes: z.string().nullable().optional(), - rating: z.number().gte(0).lte(5).nullable().optional(), - tags: z.array(z.string()).max(15).nullable().optional(), - servingStyle: ServiceStyleEnum.nullable().optional(), + notes: z.string().nullish(), + rating: z.number().gte(0).lte(5).nullish(), + tags: z.array(zTag).max(15).nullish(), + color: z.number().gte(0).lte(20).nullish(), + servingStyle: ServiceStyleEnum.nullish(), friends: z.array(z.number()).optional(), - flight: z.string().nullable().optional(), + flight: z.string().nullish(), }); diff --git a/apps/server/src/serializers/tasting.ts b/apps/server/src/serializers/tasting.ts index e79ad32ad..6ab25448e 100644 --- a/apps/server/src/serializers/tasting.ts +++ b/apps/server/src/serializers/tasting.ts @@ -109,6 +109,7 @@ export const TastingSerializer = serializer({ imageUrl: item.imageUrl ? `${config.API_SERVER}${item.imageUrl}` : null, notes: item.notes, tags: item.tags || [], + color: item.color, rating: item.rating, servingStyle: item.servingStyle, friends: attrs.friends, diff --git a/apps/server/src/trpc/routes/bottleSuggestedTagList.test.ts b/apps/server/src/trpc/routes/bottleSuggestedTagList.test.ts index 15336c8d3..f1686f528 100644 --- a/apps/server/src/trpc/routes/bottleSuggestedTagList.test.ts +++ b/apps/server/src/trpc/routes/bottleSuggestedTagList.test.ts @@ -5,6 +5,11 @@ test("lists tags", async ({ fixtures }) => { const bottle2 = await fixtures.Bottle({ brandId: bottle.brandId, }); + + const tagSolvent = await fixtures.Tag({ name: "solvent" }); + const tagCaramel = await fixtures.Tag({ name: "caramel" }); + const tagCedar = await fixtures.Tag({ name: "cedar" }); + await fixtures.Tasting({ bottleId: bottle.id, tags: ["solvent", "caramel"], @@ -27,9 +32,10 @@ test("lists tags", async ({ fixtures }) => { }); expect(results.length).toBeGreaterThan(3); - expect(results.slice(0, 3)).toEqual([ - { tag: "caramel", count: 3 }, - { tag: "cedar", count: 2 }, - { tag: "solvent", count: 1 }, - ]); + expect(results[0].tag.name).toEqual("caramel"); + expect(results[0].count).toEqual(3); + expect(results[1].tag.name).toEqual("cedar"); + expect(results[1].count).toEqual(2); + expect(results[2].tag.name).toEqual("solvent"); + expect(results[2].count).toEqual(1); }); diff --git a/apps/server/src/trpc/routes/bottleSuggestedTagList.ts b/apps/server/src/trpc/routes/bottleSuggestedTagList.ts index f0fbe2023..8be8ec895 100644 --- a/apps/server/src/trpc/routes/bottleSuggestedTagList.ts +++ b/apps/server/src/trpc/routes/bottleSuggestedTagList.ts @@ -1,6 +1,5 @@ -import { DEFAULT_TAGS } from "@peated/server/constants"; import { db } from "@peated/server/db"; -import { bottleTags, bottles } from "@peated/server/db/schema"; +import { bottleTags, bottles, tags } from "@peated/server/db/schema"; import { shuffle } from "@peated/server/lib/rand"; import { TRPCError } from "@trpc/server"; import { desc, eq, or, sql } from "drizzle-orm"; @@ -50,10 +49,12 @@ export default publicProcedure ).map((t) => [t.tag, t.total]), ); - const results = shuffle(DEFAULT_TAGS) + const defaultTags = await db.select().from(tags); + + const results = shuffle(defaultTags) .map((t) => ({ tag: t, - count: Number(usedTags[t] || 0), + count: Number(usedTags[t.name] || 0), })) .sort((a, b) => b.count - a.count); diff --git a/apps/server/src/trpc/routes/commentCreate.ts b/apps/server/src/trpc/routes/commentCreate.ts index 096d84903..9241bfb46 100644 --- a/apps/server/src/trpc/routes/commentCreate.ts +++ b/apps/server/src/trpc/routes/commentCreate.ts @@ -1,7 +1,6 @@ import { db } from "@peated/server/db"; import type { Comment, NewComment } from "@peated/server/db/schema"; import { comments, tastings } from "@peated/server/db/schema"; -import { isDistantFuture, isDistantPast } from "@peated/server/lib/dates"; import { notifyComment } from "@peated/server/lib/email"; import { createNotification } from "@peated/server/lib/notifications"; import { CommentInputSchema } from "@peated/server/schemas"; @@ -41,18 +40,6 @@ export default authedProcedure }; if (input.createdAt) { data.createdAt = new Date(input.createdAt); - if (isDistantFuture(data.createdAt, 60 * 5)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "createdAt too far in future.", - }); - } - if (isDistantPast(data.createdAt, 60 * 60 * 24 * 7)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "createdAt too far in past.", - }); - } } const user = ctx.user; diff --git a/apps/server/src/trpc/routes/tastingCreate.test.ts b/apps/server/src/trpc/routes/tastingCreate.test.ts index 60726e475..4b7c2686e 100644 --- a/apps/server/src/trpc/routes/tastingCreate.test.ts +++ b/apps/server/src/trpc/routes/tastingCreate.test.ts @@ -56,6 +56,15 @@ test("creates a new tasting with minimal params", async ({ }); test("creates a new tasting with tags", async ({ defaults, fixtures }) => { + const tags = [ + await fixtures.Tag({ + name: "cherry", + }), + await fixtures.Tag({ + name: "peat", + }), + ]; + const bottle = await fixtures.Bottle(); const caller = createCaller({ @@ -64,7 +73,7 @@ test("creates a new tasting with tags", async ({ defaults, fixtures }) => { const data = await caller.tastingCreate({ bottle: bottle.id, rating: 3.5, - tags: ["cherry", "PEAT"], + tags: [tags[0].name, tags[1].name], }); expect(data.id).toBeDefined(); @@ -76,17 +85,17 @@ test("creates a new tasting with tags", async ({ defaults, fixtures }) => { expect(tasting.bottleId).toEqual(bottle.id); expect(tasting.createdById).toEqual(defaults.user.id); - expect(tasting.tags).toEqual(["cherry", "peat"]); + expect(tasting.tags).toEqual([tags[0].name, tags[1].name]); - const tags = await db.query.bottleTags.findMany({ + const bTags = await db.query.bottleTags.findMany({ where: (bottleTags, { eq }) => eq(bottleTags.bottleId, tasting.bottleId), orderBy: (bottleTags, { asc }) => asc(bottleTags.tag), }); - expect(tags.length).toBe(2); - expect(tags[0].tag).toBe("cherry"); - expect(tags[0].count).toBe(1); - expect(tags[1].tag).toBe("peat"); - expect(tags[1].count).toBe(1); + expect(bTags.length).toBe(2); + expect(bTags[0].tag).toBe(tags[0].name); + expect(bTags[0].count).toBe(1); + expect(bTags[1].tag).toBe(tags[1].name); + expect(bTags[1].count).toBe(1); }); test("creates a new tasting with notes", async ({ defaults, fixtures }) => { diff --git a/apps/server/src/trpc/routes/tastingCreate.ts b/apps/server/src/trpc/routes/tastingCreate.ts index 0296751a3..76cb3184a 100644 --- a/apps/server/src/trpc/routes/tastingCreate.ts +++ b/apps/server/src/trpc/routes/tastingCreate.ts @@ -13,7 +13,6 @@ import { } from "@peated/server/db/schema"; import { pushJob } from "@peated/server/jobs/client"; import { checkBadges } from "@peated/server/lib/badges"; -import { isDistantFuture, isDistantPast } from "@peated/server/lib/dates"; import { notEmpty } from "@peated/server/lib/filter"; import { logError } from "@peated/server/lib/log"; import { TastingInputSchema } from "@peated/server/schemas"; @@ -22,6 +21,7 @@ import { TastingSerializer } from "@peated/server/serializers/tasting"; import { TRPCError } from "@trpc/server"; import { and, eq, inArray, sql } from "drizzle-orm"; import { authedProcedure } from ".."; +import { validateTags } from "../validators/tags"; export default authedProcedure .input(TastingInputSchema) @@ -73,25 +73,12 @@ export default authedProcedure rating: input.rating || null, flightId: flight ? flight.id : null, servingStyle: input.servingStyle || null, - tags: input.tags - ? Array.from(new Set(input.tags.map((t) => t.toLowerCase()))) - : [], + color: input.color || null, + tags: input.tags ? await validateTags(input.tags) : [], createdById: ctx.user.id, }; if (input.createdAt) { data.createdAt = new Date(input.createdAt); - if (isDistantFuture(data.createdAt, 60 * 5)) { - throw new TRPCError({ - message: "createdAt too far in future.", - code: "BAD_REQUEST", - }); - } - if (isDistantPast(data.createdAt, 60 * 60 * 24 * 7)) { - throw new TRPCError({ - message: "createdAt too far in the past.", - code: "BAD_REQUEST", - }); - } } if (input.friends && input.friends.length) { diff --git a/apps/server/src/trpc/routes/tastingUpdate.test.ts b/apps/server/src/trpc/routes/tastingUpdate.test.ts index 91d0c45eb..9265a6d56 100644 --- a/apps/server/src/trpc/routes/tastingUpdate.test.ts +++ b/apps/server/src/trpc/routes/tastingUpdate.test.ts @@ -97,6 +97,7 @@ test("updates notes", async ({ defaults, fixtures }) => { }); test("updates tags", async ({ defaults, fixtures }) => { + const tag = await fixtures.Tag(); const tasting = await fixtures.Tasting({ createdById: defaults.user.id, }); @@ -105,7 +106,7 @@ test("updates tags", async ({ defaults, fixtures }) => { }); const data = await caller.tastingUpdate({ tasting: tasting.id, - tags: ["oak"], + tags: [tag.name], }); expect(data.id).toBeDefined(); @@ -116,7 +117,7 @@ test("updates tags", async ({ defaults, fixtures }) => { .where(eq(tastings.id, data.id)); expect(omit(tasting, "tags")).toEqual(omit(newTasting, "tags")); - expect(newTasting.tags).toEqual(["oak"]); + expect(newTasting.tags).toEqual([tag.name]); const tagList = await db .select() @@ -129,6 +130,6 @@ test("updates tags", async ({ defaults, fixtures }) => { ); expect(tagList.length).toEqual(1); - expect(tagList[0].tag).toEqual("oak"); + expect(tagList[0].tag).toEqual(tag.name); expect(tagList[0].count).toEqual(1); }); diff --git a/apps/server/src/trpc/routes/tastingUpdate.ts b/apps/server/src/trpc/routes/tastingUpdate.ts index 746e079b0..36e3c51db 100644 --- a/apps/server/src/trpc/routes/tastingUpdate.ts +++ b/apps/server/src/trpc/routes/tastingUpdate.ts @@ -14,6 +14,7 @@ import { TRPCError } from "@trpc/server"; import { and, eq, gt, inArray, sql } from "drizzle-orm"; import { z } from "zod"; import { authedProcedure } from ".."; +import { validateTags } from "../validators/tags"; export default authedProcedure .input( @@ -52,6 +53,9 @@ export default authedProcedure ) { tastingData.servingStyle = input.servingStyle; } + if (input.color !== undefined && input.color !== tasting.color) { + tastingData.color = input.color; + } // TODO: needs tests yet if (input.friends && input.friends.length) { const friendUserIds = Array.from(new Set(input.friends)); @@ -73,14 +77,13 @@ export default authedProcedure } tastingData.friends = input.friends; } + if ( input.tags && input.tags !== undefined && !arraysEqual(input.tags, tasting.tags) ) { - tastingData.tags = Array.from( - new Set(input.tags.map((t) => t.toLowerCase())), - ); + tastingData.tags = await validateTags(input.tags); } const newTasting = await db.transaction(async (tx) => { diff --git a/apps/server/src/trpc/validators/tags.ts b/apps/server/src/trpc/validators/tags.ts new file mode 100644 index 000000000..a7c406c3d --- /dev/null +++ b/apps/server/src/trpc/validators/tags.ts @@ -0,0 +1,20 @@ +import { db } from "@peated/server/db"; +import { tags } from "@peated/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { inArray } from "drizzle-orm"; + +export async function validateTags(value: string[]): Promise { + if (value.length === 0) return []; + const tagList = Array.from(new Set(value.map((t) => t.toLowerCase()))); + const results = await db + .select() + .from(tags) + .where(inArray(tags.name, tagList)); + // TODO: validate each entry + if (tagList.length !== results.length) + throw new TRPCError({ + message: "One or more tag values are invalid.", + code: "BAD_REQUEST", + }); + return tagList; +} diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts index 04cb32309..7585e68ac 100644 --- a/apps/server/src/types.ts +++ b/apps/server/src/types.ts @@ -6,6 +6,7 @@ import type { EXTERNAL_SITE_TYPE_LIST, FLAVOR_PROFILES, SERVING_STYLE_LIST, + TAG_CATEGORIES, } from "./constants"; import type { BadgeSchema, @@ -34,6 +35,8 @@ import type { export type Category = (typeof CATEGORY_LIST)[number]; export type ServingStyle = (typeof SERVING_STYLE_LIST)[number]; export type FlavorProfile = (typeof FLAVOR_PROFILES)[number]; +export type TagCategory = (typeof TAG_CATEGORIES)[number]; + export type ExternalSiteType = (typeof EXTERNAL_SITE_TYPE_LIST)[number]; export type Country = (typeof COUNTRY_LIST)[number]; export type BadgeType = (typeof BADGE_TYPE_LIST)[number]; @@ -61,7 +64,12 @@ export type StorePrice = z.infer; export type Tasting = z.infer; export type User = z.infer; -export type Tag = { tag: string; count: number }; +export type Tag = { + name: string; + tagCategory: TagCategory; + flavorProfiles: FlavorProfile[]; +}; +export type SuggestedTag = { tag: Tag; count: number }; type NextPagingRel = | { diff --git a/apps/web/app/assets/serving-neat.svg b/apps/web/app/assets/serving-neat.svg new file mode 100644 index 000000000..f581c15fe --- /dev/null +++ b/apps/web/app/assets/serving-neat.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/app/assets/serving-rocks.svg b/apps/web/app/assets/serving-rocks.svg new file mode 100644 index 000000000..865323444 --- /dev/null +++ b/apps/web/app/assets/serving-rocks.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/app/assets/serving-splash.svg b/apps/web/app/assets/serving-splash.svg new file mode 100644 index 000000000..f9e86f870 --- /dev/null +++ b/apps/web/app/assets/serving-splash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/app/components/assets/ServingNeat.tsx b/apps/web/app/components/assets/ServingNeat.tsx new file mode 100644 index 000000000..ef6581689 --- /dev/null +++ b/apps/web/app/components/assets/ServingNeat.tsx @@ -0,0 +1,17 @@ +import type { SVGProps } from "react"; +const SvgServingNeat = (props: SVGProps) => ( + + + +); +export default SvgServingNeat; diff --git a/apps/web/app/components/assets/ServingRocks.tsx b/apps/web/app/components/assets/ServingRocks.tsx new file mode 100644 index 000000000..ad7f33ca2 --- /dev/null +++ b/apps/web/app/components/assets/ServingRocks.tsx @@ -0,0 +1,25 @@ +import type { SVGProps } from "react"; +const SvgServingRocks = (props: SVGProps) => ( + + + + + +); +export default SvgServingRocks; diff --git a/apps/web/app/components/assets/ServingSplash.tsx b/apps/web/app/components/assets/ServingSplash.tsx new file mode 100644 index 000000000..82e326b17 --- /dev/null +++ b/apps/web/app/components/assets/ServingSplash.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from "react"; +const SvgServingSplash = (props: SVGProps) => ( + + + + +); +export default SvgServingSplash; diff --git a/apps/web/app/components/assets/index.ts b/apps/web/app/components/assets/index.ts index e6cbe8c6a..0ed1d17e1 100644 --- a/apps/web/app/components/assets/index.ts +++ b/apps/web/app/components/assets/index.ts @@ -2,3 +2,6 @@ export { default as Bottle } from "./Bottle"; export { default as Entity } from "./Entity"; export { default as Glyph } from "./Glyph"; export { default as Logo } from "./Logo"; +export { default as ServingNeat } from "./ServingNeat"; +export { default as ServingRocks } from "./ServingRocks"; +export { default as ServingSplash } from "./ServingSplash"; diff --git a/apps/web/app/components/bottleCard.tsx b/apps/web/app/components/bottleCard.tsx index bdf9fae5f..47ad9dd1d 100644 --- a/apps/web/app/components/bottleCard.tsx +++ b/apps/web/app/components/bottleCard.tsx @@ -2,6 +2,7 @@ import { CheckBadgeIcon, StarIcon } from "@heroicons/react/20/solid"; import { formatCategoryName } from "@peated/server/lib/format"; import type { Bottle } from "@peated/server/types"; import { Link } from "@remix-run/react"; +import type { ComponentPropsWithoutRef } from "react"; import classNames from "../lib/classNames"; import BottleLink from "./bottleLink"; import type { Option } from "./selectField"; @@ -27,7 +28,7 @@ function BottleScaffold({ category: any; brand: any; statedAge: any; - color?: "default" | "highlight"; + color?: "default" | "highlight" | "inherit"; noGutter?: boolean; onClick?: () => void; }) { @@ -37,7 +38,9 @@ function BottleScaffold({ "flex items-center space-x-2 overflow-hidden sm:space-x-3", color === "highlight" ? "bg-highlight text-black" - : "bg-slate-950 text-white", + : color === "inherit" + ? "" + : "bg-slate-950 text-white", noGutter ? "" : "p-3 sm:px-5 sm:py-4", onClick ? color === "highlight" @@ -98,10 +101,11 @@ export default function BottleCard({ onClick, }: { bottle: Bottle; - noGutter?: boolean; - color?: "highlight" | "default"; onClick?: (bottle: Bottle) => void; -}) { +} & Pick< + ComponentPropsWithoutRef, + "color" | "noGutter" +>) { return ( onClick(bottle) : undefined} diff --git a/apps/web/app/components/chip.tsx b/apps/web/app/components/chip.tsx index e9ef65c76..330a19352 100644 --- a/apps/web/app/components/chip.tsx +++ b/apps/web/app/components/chip.tsx @@ -46,7 +46,9 @@ export default function Chip({ active ? "border-slate-700 bg-slate-700 text-white hover:bg-slate-700" : colorClass, - size === "small" ? "h-[24px] px-[6px] text-sm" : "h-[32px] px-[12px]", + size === "small" + ? "min-h-[24px] px-[6px] text-sm" + : "min-h-[32px] px-[12px]", )} onClick={onClick} {...props} diff --git a/apps/web/app/components/colorField.tsx b/apps/web/app/components/colorField.tsx new file mode 100644 index 000000000..c12f0faa5 --- /dev/null +++ b/apps/web/app/components/colorField.tsx @@ -0,0 +1,104 @@ +import { formatColor } from "@peated/server/lib/format"; +import { COLOR_SCALE } from "@peated/server/src/constants"; +import type { FormEvent, ReactNode } from "react"; +import { forwardRef, useState } from "react"; +import type { FieldError } from "react-hook-form"; +import classNames from "../lib/classNames"; +import FormField from "./formField"; + +type Props = { + name?: string; + label?: string; + helpText?: string; + required?: boolean; + children?: ReactNode; + className?: string; + value?: number | null; + error?: FieldError; + onChange?: (value: number | undefined) => void; +}; + +type InputEvent = FormEvent & { + target: { + value: string; + }; +}; + +export default forwardRef( + ( + { + name, + helpText, + label, + required, + className, + value: initialValue, + error, + onChange, + ...props + }, + ref, + ) => { + const [value, setValue] = useState(initialValue ?? -1); + return ( + + {value === -1 || typeof value !== "number" ? ( + "Unsure" + ) : ( + {formatColor(value)} + )} + + } + htmlFor={`f-${name}`} + required={required} + helpText={helpText} + error={error} + className={className} + > +
+
{ + e.preventDefault(); + onChange && onChange(undefined); + setValue(-1); + }} + >
+ {COLOR_SCALE.map(([num, _, hexColor]) => { + return ( + + ); + })} + { + const value = Number(e.target.value); + setValue(value); + onChange && onChange(value); + }} + /> +
+
+ ); + }, +); diff --git a/apps/web/app/components/definitionList.tsx b/apps/web/app/components/definitionList.tsx index d9250b4a0..3258ff29a 100644 --- a/apps/web/app/components/definitionList.tsx +++ b/apps/web/app/components/definitionList.tsx @@ -5,11 +5,13 @@ export function Term(props: ComponentPropsWithoutRef<"dt">) { } export function Details(props: ComponentPropsWithoutRef<"dd">) { - return
; + return ( +
+ ); } export default function DefinitionList(props: ComponentPropsWithoutRef<"dl">) { - return
; + return
; } DefinitionList.Details = Details; diff --git a/apps/web/app/components/listItem.tsx b/apps/web/app/components/listItem.tsx index 9e7f93d7d..2ca0120e2 100644 --- a/apps/web/app/components/listItem.tsx +++ b/apps/web/app/components/listItem.tsx @@ -30,7 +30,7 @@ export default function ListItem< {...props} >
-
{children}
+
{children}
); diff --git a/apps/web/app/components/rangeField.tsx b/apps/web/app/components/rangeField.tsx index 79448a701..e90b56ac4 100644 --- a/apps/web/app/components/rangeField.tsx +++ b/apps/web/app/components/rangeField.tsx @@ -82,12 +82,6 @@ export default forwardRef( onChange && onChange(e); }} /> - - - - - - ); }, diff --git a/apps/web/app/components/selectField/createOptionDialog.tsx b/apps/web/app/components/selectField/createOptionDialog.tsx index 74c4c51ae..4875a106b 100644 --- a/apps/web/app/components/selectField/createOptionDialog.tsx +++ b/apps/web/app/components/selectField/createOptionDialog.tsx @@ -6,7 +6,7 @@ import { toTitleCase } from "@peated/server/lib/strings"; import type { CreateOptionForm, Option } from "./types"; // TODO(dcramer): hitting escape doesnt do what you want here (it does nothing) -export default function CreateOptionDialog({ +export default function CreateOptionDialog({ query = "", open, setOpen, @@ -16,13 +16,13 @@ export default function CreateOptionDialog({ query?: string; open: boolean; setOpen: (value: boolean) => void; - onSubmit: (newOption: Option) => void; - render: CreateOptionForm; + onSubmit: (newOption: T) => void; + render: CreateOptionForm; }) { - const [newOption, setNewOption] = useState