Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When a model is named Set, generation fails #842

Open
nicholaschiang opened this issue Nov 24, 2023 · 1 comment
Open

When a model is named Set, generation fails #842

nicholaschiang opened this issue Nov 24, 2023 · 1 comment

Comments

@nicholaschiang
Copy link

Bug description

When a model name clashes with a built-in name (e.g. Set), generation leads to weird behavior:

(dolce-py3.11) nchiang@rowlet ~/repos/dolce (nicholas/posts) $ prisma generate
Traceback (most recent call last):
  File "/Users/nchiang/repos/dolce/.venv/bin/prisma", line 5, in <module>
    from prisma.cli import main
  File "/Users/nchiang/repos/dolce/.venv/lib/python3.11/site-packages/prisma/__init__.py", line 24, in <module>
    from .client import *
  File "/Users/nchiang/repos/dolce/.venv/lib/python3.11/site-packages/prisma/client.py", line 50, in <module>
    from . import types, models, errors, actions
  File "/Users/nchiang/repos/dolce/.venv/lib/python3.11/site-packages/prisma/types.py", line 111157, in <module>
    from . import types, enums, models, fields
  File "/Users/nchiang/repos/dolce/.venv/lib/python3.11/site-packages/prisma/models.py", line 3951, in <module>
    _User_relational_fields: Set[str] = {
                             ~~~^^^^^
TypeError: type 'Set' is not subscriptable

How to reproduce

Steps to reproduce the behavior:

  1. Create a new Prisma schema that has a model named Set
  2. Run generation once
  3. Run it again
  4. See error

Expected behavior

I should be able to name my Prisma models whatever I want to, even if they clash with built-in class names.

Prisma information

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

generator js {
  provider        = "prisma-client-js"
  previewFeatures = ["fullTextSearch"]
}

generator py {
  provider                    = "prisma-client-py"
  interface                   = "sync"
  recursive_type_depth        = 5
  enable_experimental_decimal = True
}

// A user is a person who has created an account with us.
model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The user's name, as designated by the user.
  // @todo there may be multiple users with the same name...
  name String @unique

  // The user's description (e.g. a designer biography).
  description String?

  // The user's publicly visible username, as designated by the user.
  username String? @unique

  // The user's email address, as designated by the user.
  email String? @unique

  // The user's password, stored as an encrypted hash.
  password Password?

  // The user's avatar image URL.
  avatar String? @unique

  // URL to the user's website or portfolio (e.g. journalist bio pages).
  url String? @unique

  // The articles about this user.
  articles Article[]

  // The articles written by the user.
  articlesWritten Article[] @relation("ArticlesWritten")

  // The reviews written by the user.
  reviews Review[]

  // The posts uploaded by the user (this does not mean that the post was
  // originally authored by this user, but that it was added to our database by
  // this user).
  posts Post[]

  // The user's sets (i.e. arbitrary groupings of saved looks).
  sets Set[]

  // The looks this user has authored.
  looks Look[]

  // Whether the user is a curator (i.e. someone who can edit shows, etc).
  curator Boolean @default(false)

  // DESIGNER FIELDS - only applicable to fashion designers.

  // Where the designer purports to be from.
  country   Country? @relation(fields: [countryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  countryId Int?

  // The collections that the designer created or otherwise curated.
  collections Collection[]

  // The products that the designer designed.
  products Product[]

  // MODEL FIELDS - only applicable to fashion models.

  // The looks that this model wore during runway shows.
  looksModeled Look[] @relation("LooksModeled")
}

// A user's password, stored in Postgres as an encrypted hash.
model Password {
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The securely encrypted hash of the user's original password text.
  hash String

  // The user whose password this is.
  user   User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId Int  @unique
}

// A company is a legal corporation. Companies can own many brands.
// e.g. The LVMH company owns Louis Vuitton, Dior, Givenchy, etc.
model Company {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The corporations legal name.
  name String @unique

  // The company's avatar URL (i.e. logo).
  avatar String? @unique

  // URL to the company's website.
  url String? @unique

  // A short description of the company, typically sourced from Wikipedia.
  description String?

  // The brands owned and operated by the corporation.
  brands Brand[]

  // The retailers owned and operated by the corporation.
  retailers Retailer[]

  // The country where the corporation is legally headquartered.
  country   Country @relation(fields: [countryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  countryId Int

  // @todo perhaps store links to where this information was sourced from?
}

// A retailer is a recognizable commerce entity that sells products. Note that
// this is different than a company to allow companies to own many retailers.
// e.g. Neiman Marcus, Nordstrom, GOAT, StockX, Ebay, etc.
model Retailer {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The retailer's most recognizable name, styled in their preferred format.
  name String @unique

  // The retailer's avatar URL (i.e. logo).
  avatar String? @unique

  // URL to the retailer's website.
  url String? @unique

  // The company that owns and operates the retailer.
  company   Company? @relation(fields: [companyId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  companyId Int?

  // A short description of the retailer, typically sourced from Wikipedia.
  description String?

  // The brands sold by the retailer.
  // @todo perhaps this shouldn't be an explicit relation but rather implied by
  // the products (and their associated brands) that the retailer sells.
  brands Brand[]

  // The prices (associated with products) sold by the retailer.
  prices Price[]

  // The countries in which the retailer operates.
  countries Country[]

  // The links (to collections or brands) on this retailer's website.
  links Link[]
}

// Tiers attempt to encapsulate a brand's reputation, business model, and prices:
// 
// 0 - $50k-‚àû bespoke. does not sell to the general public. 
// 1 - $5-50k superpremium.  e.g. Patek Philippe, Bottega, Hermes 
// 2 - $1500-5k premium core. e.g. Rolex, Berluti, Omega, Cartier
// 3 - $300-1500 accessible core. e.g. GUCCI, Prada, Tod's, Montblanc
// 4 - $100-300 affordable luxury. e.g. Coach, Geox
// 
// 5 - $80-$700 diffusion. secondary lines by luxury names. e.g. Marc by Marc Jacobs
// 6 - $40-500 high-end street. e.g. All Saints, Coast
// 7 - $20-120 mid-level high street. e.g. Topshop, M&S
// 8 - $5-30 value market. relies on huge sales. e.g. Primark, Shein, Walmart
// 
// https://createafashionbrand.com/the-many-market-levels-of-fashion-brands/
// https://www.businessinsider.com/pyramid-of-luxury-brands-2015-3
//
// @todo perhaps this should be a model of its own?
enum Tier {
  BESPOKE
  SUPERPREMIUM
  PREMIUM_CORE
  ACCESSIBLE_CORE
  AFFORDABLE_LUXURY
  DIFFUSION
  HIGH_STREET
  MID_STREET
  VALUE_MARKET
}

// A brand is a recognizable name. Brands with similar names are given tiers.
// e.g. GUESS is given tier 1 while GBG and GUESS FACTORY are given tier 2.
//
// Typically, a brand will be the name that appears on that tags of products.
// Occasionally, a brand will not have its own products (e.g. "Fashion East" is
// considered a brand even though they do not create their own products; they
// simply showcase other designer's brand's clothing at their runway shows).
model Brand {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The brand's most recognizable name, styled in the brand's preferred format.
  name String @unique

  // The URL friendly slug identifier for the brand. This is different than the
  // integer ID column as I want to have a user-friendly URL for each brand.
  // Ex: /shows/resort-2024/hermes is better than /shows/356 for SEO.
  // @see {@link https://linear.app/nicholaschiang/issue/NC-673}
  slug String @unique

  // The brand's avatar URL (i.e. logo).
  avatar String? @unique

  // URL to the brand's website.
  url String? @unique

  // A short description of the brand, typically sourced from Wikipedia.
  description String?

  // The brand's tier. This will be NULL if it has not been assigned yet.
  // @todo perhaps rename this to "BrandTier" to avoid confusion with "Level"? 
  tier Tier?

  // The company that owns and operates the brand (if known).
  company   Company? @relation(fields: [companyId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  companyId Int?

  // The products designed or otherwise produced by the brand.
  products Product[]

  // The runway shows presented by the brand.
  shows Show[]

  // The sizes used by the brand.
  sizes Size[]

  // The prices (associated with products) sold by the brand (i.e. MSRPs).
  prices Price[]

  // Links to the brand's page on retailer sites.
  links Link[]

  // The retailers that sell the brand.
  retailers Retailer[]

  // The collections designed by the brand.
  collections Collection[]

  // The country the brand purports to be from (if known).
  country   Country? @relation(fields: [countryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  countryId Int?
}

// A country is a sovereign state. Countries can have many brands and sizes.
model Country {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The country's full name, as designated by the United Nations.
  name String @unique

  // The designers that purport to be from the country.
  designers User[]

  // The companies that are legally headquartered in the country.
  companies Company[]

  // The brands that purportedly originate from the country.
  brands Brand[]

  // The retailers that operate in the country.
  retailers Retailer[]

  // The country's nationwide standardized sizes.
  sizes Size[]
}

// A style group represents a collection of mutually exclusive styles.
// Allegorical to Linear's label groups (you can only filter on one at a time).
// e.g. the "Neckline" style group contains "Crewneck", "V-Neck", etc (a product
// can not have a crewneck and a v-neck at the same time).
model StyleGroup {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The style group's name.
  name String @unique

  // The styles that belong to the style group.
  styles Style[]
}

// A product style category is a high-level grouping of products. Styles are a
// tad bit reminiscent of the typical issue tracking tool's "labels" feature.
// e.g. blazer, bomber, cardigan, quilted, raincoat, jeans, tuxedos, etc.
model Style {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The style category's name, styled in the preferred format.
  name String @unique

  // The products that belong to the style category.
  products Product[]

  // The sizes used by the style category.
  sizes Size[]

  // The items that have this style (e.g. "turtleneck").
  items Item[]

  // The collections that exclusively contain products from this style.
  collections Collection[]

  // The style group that the style belongs to, if any.
  styleGroup   StyleGroup? @relation(fields: [styleGroupId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  styleGroupId Int?

  // The style subcategories that can be nested underneath this style.
  // e.g. tops > t-shirts > crew neck, tops > t-shirts > v-neck, etc.
  parentId Int?
  parent   Style?  @relation("ParentChildStyle", fields: [parentId], references: [id])
  children Style[] @relation("ParentChildStyle")
}

// A size is a measurement of a product's dimensions. Sizes can either be owned
// by a brand (for proprietary brand specific sizing systems) or a country (for
// nationwide standardized sizes). Users can then add multiple sizes to their 
// profile. Our system will automatically suggest sizes to add based on the 
// user's previous purchases and existing profile sizes.
model Size {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The size's name, as designated by the brand or country.
  name String

  // A unique slug derived from the name, sex, style, and brand. This exists
  // primarily to make imports easier (i.e. we can use the slug to match sizes
  // in a connectOrCreate statement instead of having to fetch the styleId).
  slug String @unique

  // The product style the size is used for (e.g. tops, outerwear, puffers).
  style   Style @relation(fields: [styleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  styleId Int

  // The original intended sex the size is specifying for.
  sex Sex

  // The size's chest measurement (cm) as designated by the brand.
  chest Decimal?

  // The size's shoulder measurement (cm) as designated by the brand.
  shoulder Decimal?

  // The size's waist measurement (cm) as designated by the brand.
  waist Decimal?

  // The size's sleeve measurement (cm) as designated by the brand.
  sleeve Decimal?

  // The brand whose size this is.
  brand   Brand? @relation(fields: [brandId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  brandId Int?

  // The country whose size this is.
  country   Country? @relation(fields: [countryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  countryId Int?

  // Equivalent sizes. A size can have zero or more equivalent sizes.
  equivalents  Size[] @relation("SizeEquivalents")
  equivalentOf Size[] @relation("SizeEquivalents")

  // The product variants that are available in this size.
  variants Variant[]

  // @todo ensure that a size always has either a country or a brand.
  // @see https://github.com/prisma/prisma/issues/17319

  // Each brand or country must have unique size names per category and sex. 
  @@unique([name, sex, styleId, brandId, countryId])
}

// A color is a label assigned to products by their designers.
// @todo perhaps standardize this by associating each color with an RGBA range?
model Color {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The color's name, as designated by the brand (e.g. "Beige", "Black", etc).
  name String @unique

  // The product variants that are available in this color.
  variants Variant[]

  // The items that have this color.
  items Item[]

  // @todo perhaps colors should be associated with brands? e.g. "Gucci Beige"?
}

// A sustainability is a label indicating some level of sustainability.
enum Sustainability {
  RECYCLED // Certified recycled materials. 
  ORGANIC // Certified organic materials.
  RESPONSIBLE_DOWN // Responsible Down Standard cerified.
  RESPONSIBLE_FORESTRY // Wood-based fabrics from sustainably managed forests.
  RESPONSIBLE_WOOL // Responsible Wool Standard certified.
  RESPONSIBLE_CASHMERE // Responsible Cashmere Standard certified.
}

// A material is a fabric or other ingrediant used to formulate a product.
model Material {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The material's name, as designated by industry standard (e.g. "Cotton") or
  // the brand for proprietary fabrics (e.g. "Bombtwill", "City Wool").
  name String @unique

  // The material's description, as designated by the brand (e.g. for
  // proprietary fabrics like LENZING ECOVERO Viscose) or industry standard.
  description String?

  // The material's sustainability status, if any.
  sustainability Sustainability?

  // The product variants that are available in this material.
  variants Variant[]

  // The style subcategories that can be nested underneath this style.
  // e.g. Viscose > LENZING ECOVERO Viscose, Wool > Merino > City Wool, etc.
  parentId Int?
  parent   Material?  @relation("ParentChildMaterial", fields: [parentId], references: [id])
  children Material[] @relation("ParentChildMaterial")

  // @todo perhaps materials should be associated with brands or countries
  // similar to how sizes are associated with either a brand or country?

  // @todo perhaps create a manufacturer model to associate with materials?
}

// A tag is an arbitrary label applied by a brand or retailer to their items.
// This was added primarily to preserve information from scraping Shopify. These
// often correlate with specific collections, seasons, or styles. There's no
// easy way to classify them at save time, so I just include them and will run
// SQL queries manually to re-classify them.
model Tag {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The tag's name, as designated by the brand or retailer.
  name String @unique

  // The product variants that are available in this tag.
  variants Variant[]
}

// A variant specifies the properties of an item you can purchase. Each variant
// is associated with a unique SKU number. Variants are identified primarily by
// size and color. Each variant has prices from different vendors.
model Variant {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The variant's SKU, as designated by the brand.
  sku String @unique

  // The product the variant is of.
  product   Product @relation(fields: [productId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  productId Int

  // The colors the variant consists of. Typically a single color but can be
  // multiple if the variant contains a gradient or a mix of multiple colors.
  colors Color[]

  // The materials (a.k.a. fabrics) that the variant is made of.
  materials Material[]

  // The size of the variant.
  size   Size @relation(fields: [sizeId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  sizeId Int

  // Images and videos of the product variant being modeled.
  videos Video[]
  images Image[]

  // Arbitrary tags applied by the brand or retailer.
  tags Tag[]

  // The prices that are associated with the variant. Typically a single price
  // but can be multiple if the item is sold by multiple retailers or if there
  // are different prices per size. Can be empty if the variant is sold out.
  prices Price[]

  // The user-curated sets that the product variant is a part of.
  sets Set[]

  // The posts that include this product variant.
  posts Post[]

  // @todo each product can only have a single variant per color and size combo.
}

// A sex is an arbitrary label designated by a brand or designer to indicate a 
// product's originally intended consumer.
enum Sex {
  MAN
  WOMAN
  UNISEX
}

// A market is either primary (MSRP and retailers) or secondary (resale).
enum Market {
  PRIMARY
  SECONDARY
}

// A price is an encapsulation of a product's value. A price can be for all the
// sizes and color variants of a product (e.g. when being sold at retail value)
// or specific to a single size and color variant (e.g. GOAT, Ebay, StockX).
model Price {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The price's value in USD.
  value Decimal

  // The price's market (primary—MSRP and retailers—or secondary—resale value).
  market Market

  // The URL of the product's listing at this price.
  url String

  // The retailer that sells the product at this price.
  retailer   Retailer? @relation(fields: [retailerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  retailerId Int?

  // The brand that sells the product at this price.
  brand   Brand? @relation(fields: [brandId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  brandId Int?

  // The product variant sold at this price (the size and color combo).
  variant   Variant @relation(fields: [variantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  variantId Int

  // Whether or not the price is still available (e.g. if it is sold out). This
  // is stored on the price model instead of the variant model as different
  // vendors can have different stocks of a given variant.
  // @todo instead of this, perhaps we should have a "stock" numeric field that
  // tracks how many units are available at this price from this retailer?
  available Boolean @default(true)

  // @todo ensure that a price always has either a retailer or a brand.
  // @see https://github.com/prisma/prisma/issues/17319

  // Each price must have a unique value and URL (note that I can't simply put a
  // unique constraint on the URL due to secondary markets like GOAT that have a
  // single URL for many different sizes at many different prices).

  // Because each price contains the stock information for a specific size and
  // color variant, each price must be unique to that variant.
  @@unique([variantId, value, url])
}

// An image. Typically of a product being modeled.
// @todo perhaps we should also store the image's original source?
model Image {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The URL (either fully qualified or a relative path) to the largest size of
  // the image available (the front-end optimizes images at runtime).
  url String @unique

  // The image position in the product or look's gallery (if applicable).
  // @todo enforce unique image positions per product, variant, or look.
  position Int?

  // The image width (if known) in px.
  width Int?

  // The image height (if known) in px.
  height Int?

  // The product variant(s) the image is of.
  // @todo if multiple products refer to the same image, we should make it
  // associated with a look instead and then link the look with those products.
  variants Variant[]

  // The runway look the image is of.
  look   Look? @relation(fields: [lookId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  lookId Int?

  // The post that the image is from.
  post   Post? @relation(fields: [postId], references: [id])
  postId Int?

  // @todo store information on the models in the image (e.g. insta, etc).
}

// A video. Typically of a product being modeled.
// @todo perhaps we should also store the image's original source?
model Video {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The video's URL (either fully qualified or a relative path).
  url String @unique

  // The video's mime type.
  mimeType String

  // The product variant(s) the video is of.
  variants Variant[]

  // The show the video is of.
  show Show?

  // The post that the video is from.
  post   Post? @relation(fields: [postId], references: [id])
  postId Int?

  // @todo store information on the models in the video (e.g. insta, etc).
}

// Levels attempt to encapsulate a product's quality, price, and availability:
// 
// 0 - bespoke. made to measure e.g. by comission.
// 1 - haute couture. handmade approved by french law.
// 2 - handmade. e.g. one-of-one etsy items, products made a friend.
// 3 - ready-to-wear. widely available online or in-store.
enum Level {
  BESPOKE
  COUTURE
  HANDMADE
  RTW
}

// An item is a high-level product category (e.g. "white turtleneck sweater").
model Item {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The item's styles (e.g. "turtleneck", "sweater").
  styles Style[]

  // The item's colors (e.g. "white").
  colors Color[]

  // Products that satisfy the item specifications (i.e. a turtleneck sweater
  // sold by Aritzia that has a white color variant).
  //
  // Note that I intentionally do not associate variants with items. Instead, it
  // will be up to the client (i.e. the front-end) to show the correct product
  // variant that has the item's correct colors.
  products Product[]

  // The posts that include this item category in them.
  posts Post[]
}

// A product is an item that can be bought and sold.
model Product {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The product's name as designated by the brand and designer.
  name String @unique

  // The URL friendly slug identifier for the product.
  // Ex: /products/hermes-frozen-shorts is better than /products/356 for SEO.
  // @see {@link https://linear.app/nicholaschiang/issue/NC-673}
  slug String @unique

  // The product's description.
  description String?

  // The product's level.
  // @todo perhaps rename this to "ProductLevel" to avoid confusion with "Tier"? 
  level Level

  // The variants (colors + materials + sizes) the product was made in.
  variants Variant[]

  // The original MSRP value of the product in USD (if applicable).
  // @todo perhaps this should be a field on the variant?
  msrp Decimal?

  // When a product was originally conceived.
  designedAt DateTime

  // When a product was first available to be purchased.
  releasedAt DateTime

  // The product's styles. Allegorical to labels (e.g. top, t-shirt, v-neck).
  // @todo preserve relationships between the same product in two different 
  // styles (e.g. the "Agency Pant" and the "Agency Cropped Pant").
  styles Style[]

  // Item specifications that the product satisfies (e.g. a turtleneck sweater).
  items Item[]

  // The collections that feature the product.
  collections Collection[]

  // The product's designers. Typically, this will be a single person.
  designers User[]

  // The product's brands. Collaborations can have multiple brands.
  brands Brand[]

  // The product's looks (runway outfits that it was included in).
  looks Look[]

  // The posts that include this item.
  posts Post[]

  // @todo perhaps we should also store the product's original source?

  // @todo products must have a unique name per brand(s).
}

// A set is an arbitrary grouping of looks created by a user to act as a sort of
// mood board. Users can "save" looks that they like to a "set" that can then be 
// shared with other users. Users can discover sets by other users (e.g. I see a
// look that I like and then expore all the sets that include that look to find
// similar looks).
//
// Sets are separate from collections for simplicity. Collections are officially
// curated by a brand while sets are simple groupings of looks and products
// created by users. I may opt to combine these two data models in the future, 
// but for now, simply adding an additional data model was easiest.
//
// Users can import looks and products from anywhere to add to their sets (e.g.
// if I see an outfit I like on Instagram, I can click "share" to DOLCE and the
// app will add it as a look to the selected set... that look will then be
// automatically augmented with possible products to purchase).
//
// The name "set" was inspired by the website "Polyvore" which allowed users to
// add products to a shared index called a "set".
// @see {@link https://en.wikipedia.org/wiki/Polyvore}
model Set {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The set's name (e.g. "Summer Essentials").
  name String

  // The set's description.
  description String?

  // The set's author (i.e. the user who created the set).
  // @todo I should support multiple author(s) for a set (i.e. shared "sets") or
  // perhaps advanced access control (i.e. users who can view, can edit, etc).
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId Int

  // The set's looks.
  looks Look[]

  // The set's product variants.
  // @todo perhaps we should also allow users to add products to a set without
  // having to select a specific size and/or color (e.g. when they're adding
  // products from the products list, they do not select a size)?
  variants Variant[]

  // A user can only have one set with a given name.
  @@unique([name, authorId])
}

// A collection is an arbitrary grouping of products, typically done by a brand
// or a designer. Often, collections are created entirely by a single designer.
// Collections are separate from shows as every show has a collection but not
// every collection has a show (e.g. Mission Workshop "Merino Core" collection
// has no show v.s. Saint Laurent Fall-Winter 2023 has a show). Each show can
// also have multiple collections shown (e.g. Fashion East often showcases
// three collections from three different designers on the same runway).
model Collection {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The collection's name (e.g. "Hermès Spring-Summer 2023 Menswear").
  name String @unique

  // The collection's style category (if limited to a single category).
  style   Style? @relation(fields: [styleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  styleId Int?

  // The collection's season. Typically, collection have seasons. While unusual,
  // collections can be released outside of a season (e.g. mw acre series).
  season   Season? @relation(fields: [seasonId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  seasonId Int?

  // The show that the collection was debuted at.
  show   Show? @relation(fields: [showId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  showId Int?

  // The collection's webpage (from the designer, brand, or retailer site).
  links Link[]

  // The products that belong to the collection.
  products Product[]

  // The designers that created the collection. Often, this is one person.
  // @todo products are already associated with designers; do we need this?
  designers User[]

  // The brands that created the collection. Generally, we will only have one 
  // brand, but—according to ChatGPT—there have been runway collections that 
  // have been operated by multiple brands and showcased pieces from both of the 
  // brands. One example is the "Fashion East" show in London, which provides a 
  // platform for emerging designers to showcase their collections. The show 
  // often features a combination of individual designers and collaborative 
  // collections. Another example is the "Designer Collaborations" show at New 
  // York Fashion Week, which features collaborations between established 
  // designers and brands.
  // @todo products are already associated with brands; do we need this?
  brands Brand[]
}

// A link is exactly that. A link to an external website. Currently, this model
// is just used for collections, but will likely be used more in the future.
model Link {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The link's URL.
  url String @unique

  // The collection the link is associated with.
  collection   Collection? @relation(fields: [collectionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  collectionId Int?

  // The brand that the link is associated with.
  brand   Brand? @relation(fields: [brandId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  brandId Int?

  // The retailer that the link is associated with.
  retailer   Retailer? @relation(fields: [retailerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  retailerId Int?

  // Each collection can only have one link per brand or retailer.
  @@unique([collectionId, brandId])
  @@unique([collectionId, retailerId])
  // Each brand can only have one link per retailer.
  @@unique([brandId, retailerId])
}

// A widely accepted and used season name.
//
// While brands may use different season names, these are the ones used by Vogue 
// and/or WWD (where I'm scraping data from), and thus these are the ones I use.
enum SeasonName {
  RESORT
  SPRING
  PRE_FALL
  FALL
}

// A fashion season is a widely accepted grouping of fashion releases.
model Season {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The name of the season, as widely accepted and recognized.
  name SeasonName

  // The year the season takes place in.
  year Int

  // The runway shows that took place during the season.
  shows Show[]

  // The collections that were released during the season.
  collections Collection[]

  // Each season must have a unique name and year.
  @@unique([name, year])
}

// A look is an outfit that a model wore during a runway show.
model Look {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The look's number. Typically, looks are numbered sequentially.
  number Int

  // The look's show.
  show   Show? @relation(fields: [showId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  showId Int?

  // The look's author (if not part of a show, this was created by a user).
  author   User? @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId Int?

  // @todo ensure that a look always has either a show or an author.
  // @see https://github.com/prisma/prisma/issues/17319

  // The look's model (if known).
  model   User? @relation("LooksModeled", fields: [modelId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  modelId Int?

  // The look's products (if known).
  // @todo perhaps I should optionally include variants if a specific size/color
  // of the product is shown in the look images and that information is known.
  products Product[]

  // The look's picture (if known).
  images Image[]

  // The user-curated sets that the look is a part of.
  sets Set[]

  // The posts that the look is a part of.
  posts Post[]

  // Each look must have a unique number in the show or by the author.
  @@unique([showId, number])
  @@unique([authorId, number])
}

// A post is an Instagram or TikTok or Threads or other social media post that
// contains product(s) or look(s).
model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The post's original URL.
  url String @unique

  // The post's original description (i.e. the caption).
  description String?

  // The post's author (this is not necessarily the original author but the user
  // that brought the post unto the DOLCE platform).
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId Int

  // The post's images.
  images Image[]

  // The post's videos.
  videos Video[]

  // The post's items (overall type of item e.g. "white turtleneck sweater").
  items Item[]

  // The post's products (exact brand of the item if known).
  products Product[]

  // The post's variants (exact color/size of the product if known).
  variants Variant[]

  // The post's looks (exact collection of looks).
  looks Look[]
}

// An article is exactly that: a work of writing about some fashion-related 
// topic. For now, these generally fall into two categories:
// - biographies about fashion designers (e.g. imported from Wikipedia);
// - critic reviews of fashion shows (e.g. imported from Vogue and WWD).
// @see {@link https://linear.app/nicholaschiang/issue/NC-658}
model Article {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The date when the article was originally written or last edited (if known).
  // 
  // The date when the article was originally written and the time when I 
  // imported it will often be different (which is why this field exists 
  // separately from the `createdAt` database field).
  writtenAt DateTime?

  // The article's canonical URL.
  url String @unique

  // The article title (usually included as the header on their webpage).
  title String

  // The article subtitle (typically included below the title on their webpage).
  // This is often a short summary of the general gist of the article content.
  // This field differs from the `summary` field as it is author-provided. The
  // `summary` field is generated by OpenAI or written by one of our curators.
  subtitle String?

  // The article summary (a plain text string; generated via OpenAI).
  summary String?

  // The article content (an HTML string).
  content String

  // The article review sentiment score from 0-1 (if applicable).
  //
  // A score of 0.5 is neutral, 0 is negative, and 1 is positive.
  // 
  // Critical reviews will use whatever scale the critic uses (e.g. 0-10) or
  // will revert back to using a five-star scale if the critic does not assign
  // a score in their review (it will then be assigned via OpenAI).
  //
  // This field should only ever be NULL if a critic review has been imported
  // but no score has been assigned to it yet (e.g. when scraping Vogue) or if
  // this article simply isn't a critic review.
  score Decimal?

  // The article author (if applicable; Wikipedia has too many authors).
  author   User? @relation("ArticlesWritten", fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId Int?

  // The user that the article is about (if this is a designer biography).
  user   User? @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId Int?

  // The show that the review is about (if this is a critic review).
  show   Show? @relation(fields: [showId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  showId Int?

  // The publication that the article was posted to (Vogue, WWD, Wikipedia).
  publication   Publication @relation(fields: [publicationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  publicationId Int

  // Each publication can only have one canonical article about a topic. This
  // constraint exists primarily to ensure that I don't import duplicates.
  // @@unique([publicationId, userId])
  // @@unique([publicationId, showId])

  // Each author can only submit one review for a show. I've yet to encounter
  // the same journalist publishing two different reviews for the same show. If
  // that does happen, I can always just replace their review with whichever is
  // the most recent. If a single journalist publishes two reviews in two
  // different publications for the same show, I should only count one of them
  // towards the aggregate critic score.
  @@unique([authorId, showId])
  // Each publication can only have one canonical article with a given title.
  @@unique([publicationId, title])
}

// A review is a review for a show from a consumer.
model Review {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The review sentiment score from 0-1.
  //
  // A score of 0.5 is neutral, 0 is negative, and 1 is positive.
  // 
  // This will always increment by 0.2 (as we use a five-star scale to assign 
  // these score numbers for consumer reviews).
  score Decimal

  // The review content (a plain text string).
  content String

  // The review author.
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  authorId Int

  // The show that the review is about.
  show   Show @relation(fields: [showId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  showId Int

  // Each user can only submit one review per show.
  @@unique([authorId, showId])
}

// A publication is a resource that publishes fashion reviews (e.g. Vogue).
model Publication {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The publication's name.
  name String @unique

  // The publication's avatar URL (i.e. logo).
  avatar String? @unique

  // The publication's articles.
  articles Article[]
}

// There are only a few locations where fashion shows are held. While these may
// not always be entirely accurate, they are more of a category of shows (e.g.
// "Spring Tokyo 2023") than a specific location.
//
// This field was inspired by the Vogue season names (e.g. "Tokyo Spring 2023")
// and the drop-down included on the WWD website (e.g. "New York", "Paris").
//
// @todo replace the "BRIDAL" location with some other show flag.
// @todo this really should probably be its own model instead of an enum.
enum Location {
  NEW_YORK
  LONDON
  MILAN
  PARIS
  TOKYO
  BERLIN
  FLORENCE
  LOS_ANGELES

  MADRID
  COPENHAGEN
  SHANGHAI
  AUSTRALIA
  STOCKHOLM
  MEXICO
  MEXICO_CITY
  KIEV
  TBILISI
  SEOUL
  RUSSIA
  UKRAINE
  SAO_PAOLO

  BRIDAL
}

// A show is a fashion runway show.
model Show {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The name of the show, as designated by the show's organizer.
  // @todo often this will be the same as the collection name; do we need this?
  // @todo often this is just a function of the brand + season names...
  name String @unique

  // The link to the show on the brand website (where the description is from).
  url String @unique

  // The show's sex (i.e. "womenswear", "menswear", or both).
  // @todo remove this once we have full show > collection > product data, as
  // the product object already has the sex field attached to it.
  sex Sex

  // The show's level (i.e. "ready-to-wear", "couture", etc).
  // @todo remove this once we have full show > collection > product data, as
  // the product object already has the level field attached to it.
  level Level

  // A description of the collection, typically provided by the brand.
  description String?

  // The critic's consensus on the show.
  articlesConsensus String?

  // The articles about the show (i.e. the show's critic reviews).
  articles Article[]

  // The consumer's consensus on the show.
  reviewsConsensus String?

  // Consumer reviews of the show.
  reviews Review[]

  // Video of the show (typically just a single shot of runway models walking).
  video   Video? @relation(fields: [videoId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  videoId Int?   @unique

  // The fashion season in which the show was presented. e.g. Spring 2021
  // @todo collections are already associated with seasons; do we need this?
  season   Season @relation(fields: [seasonId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  seasonId Int

  // The date when the show started (if known).
  date DateTime?

  // The runway show's location.
  location Location?

  // The collections that were presented at the show. Often, there is only one.
  collections Collection[]

  // The looks that were presented at the show.
  looks Look[]

  // The brand that hosted the show. This is different than the collection(s)
  // brand(s) that were presented at the show. Often, they will be the same.
  // Occasionally, however, the brand that hosts the show (e.g. "Fashion East")
  // will not be the brand(s) that presented their products at the show.
  brand   Brand @relation(fields: [brandId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  brandId Int

  // A brand can only have a single show per season per sex (i.e. "menswear",
  // "womenswear", or both) per level (i.e. "couture", "ready-to-wear", etc) per
  // location (e.g. an "Australia" collection alongside a "Paris" collection).
  //
  // Ex: https://www.vogue.com/fashion-shows/australia-spring-2015/maticevski
  // Ex: https://www.vogue.com/fashion-shows/spring-2015-ready-to-wear/maticevski
  //
  // This constraint was inspired by Vogue's URLs (e.g. /resort-2024/hermes). It 
  // may need adjustment in the future if there is ever a brand that presents 
  // twice during a single season (but that probably means I need more seasons).
  @@unique([brandId, seasonId, sex, level, location])
}

Environment & setup

  • OS: Mac OS
  • Database: PostgreSQL
  • Python version: Python 3.11.4
  • Prisma version: 5.4.2
prisma                  : 5.4.2
prisma client python    : 0.11.0
platform                : darwin
expected engine version : ac9d7041ed77bcc8a8dbd2ab6616b39013829574
installed extras        : []
install path            : /Users/nchiang/repos/dolce/.venv/lib/python3.11/site-packages/prisma
binary cache dir        : /Users/nchiang/.cache/prisma-python/binaries/5.4.2/ac9d7041ed77bcc8a8dbd2ab6616b39013829574
@tylerexpa
Copy link

I hit a similar problem with an enum value named "global".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants