RobertCraigie / prisma-client-py

Prisma Client Python is an auto-generated and fully type-safe database client designed for ease of use
https://prisma-client-py.readthedocs.io
Apache License 2.0
1.76k stars 71 forks source link

When a model is named `Set`, generation fails #842

Open nicholaschiang opened 7 months ago

nicholaschiang commented 7 months ago

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

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 commented 7 months ago

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