packages feed

notion-client-0.9.0.0: src/Notion/V1/PropertyValue.hs

-- | Typed property values for Notion pages.
--
-- A property value represents the actual data stored in a page's property —
-- for example, the selected option in a select property, or the text in a title.
-- This is distinct from 'Notion.V1.Properties.PropertySchema' which describes
-- the shape/configuration of the property column.
--
-- This module provides:
--
-- * A 'PropertyValue' sum type for reading page properties via pattern matching
-- * Smart constructors ('titleValue', 'selectValue', etc.) for writing properties
-- * 'propertyValueResponseJSON' for exporting modeled response data
-- * Supporting types ('SelectOptionValue', 'FileValue', 'RelationRef', etc.)
module Notion.V1.PropertyValue
  ( -- * Property value
    PropertyValue (..),
    propertyValueResponseJSON,

    -- * Supporting types
    SelectOptionValue (..),
    FileValue (..),
    RelationRef (..),
    FormulaResult (..),
    RollupResult (..),
    UniqueIdResult (..),
    VerificationResult (..),
    VerificationState (..),
    Place (..),

    -- * Smart constructors
    titleValue,
    richTextValue,
    numberValue,
    selectValue,
    multiSelectValue,
    dateValue,
    checkboxValue,
    urlValue,
    emailValue,
    phoneNumberValue,
    relationValue,
    statusValue,
    peopleValue,
    peopleEntriesValue,
    placeValue,
    verifiedValue,
    unverifiedValue,
    filesValue,
    fileUploadFilesValue,
  )
where

import Data.Aeson ((.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (Pair)
import Data.Maybe (fromMaybe)
import Data.Scientific (Scientific)
import Data.Vector qualified as Vector
import Notion.Prelude
import Notion.V1.Common (ExternalFile (..), File, UUID (..))
import Notion.V1.Properties (RollupFunction)
import Notion.V1.RichText (Date (..), MentionContent (..), RichText (..), RichTextContent (..))
import Notion.V1.Users (PeopleEntry (..), UserReference (..), UserValue (..))
import Notion.V1.Users qualified as Users
import Prelude hiding (id)

-- | A typed property value from a Notion page.
--
-- Each constructor carries the property's schema ID as its first 'Text' field.
-- When reading from the API this is the property's internal ID (e.g., @\"abc\"@).
-- When constructing values for writes, use @\"\"@ (the smart constructors do this).
--
-- Read-only variants ('FormulaValue', 'RollupValue', 'UniqueIdValue',
-- 'CreatedTimeValue', 'CreatedByValue', 'LastEditedTimeValue', 'LastEditedByValue',
-- 'VerificationValue') only appear in API responses.
--
-- A property value of a type this library does not know decodes as
-- 'UnknownPropertyValue'. Values nested in a rollup array carry no ID; their
-- first field is @\"\"@.
data PropertyValue
  = TitleValue Text (Vector RichText)
  | RichTextValue Text (Vector RichText)
  | NumberValue Text (Maybe Scientific)
  | SelectValue Text (Maybe SelectOptionValue)
  | MultiSelectValue Text (Vector SelectOptionValue)
  | DateValue Text (Maybe Date)
  | PeopleValue Text (Vector PeopleEntry)
  | FilesValue Text (Vector FileValue)
  | CheckboxValue Text Bool
  | UrlValue Text (Maybe Text)
  | EmailValue Text (Maybe Text)
  | PhoneNumberValue Text (Maybe Text)
  | FormulaValue Text FormulaResult
  | -- | The supplied completeness flag: @Just True@ means the references are
    -- incomplete, @Just False@ means complete, and @Nothing@ means unspecified.
    -- No flag is inferred from the reference count and no extra request is made.
    RelationValue Text (Vector RelationRef) (Maybe Bool)
  | RollupValue Text RollupResult
  | CreatedTimeValue Text Text
  | CreatedByValue Text UserReference
  | LastEditedTimeValue Text Text
  | LastEditedByValue Text UserReference
  | StatusValue Text (Maybe SelectOptionValue)
  | UniqueIdValue Text UniqueIdResult
  | PlaceValue Text (Maybe Place)
  | ButtonValue Text (Maybe Value)
  | VerificationValue Text (Maybe VerificationResult)
  | -- | A property type this library does not model yet: the property ID, the
    -- type name and the raw value under the type key ('Null' when absent).
    UnknownPropertyValue Text Text Value
  deriving stock (Show)

instance FromJSON PropertyValue where
  parseJSON = \case
    Object o -> do
      pid <- fromMaybe "" <$> o .:? "id"
      propType <- o .: "type"
      let key = Key.fromText propType
      case propType of
        "title" -> TitleValue pid <$> o .: key
        "rich_text" -> RichTextValue pid <$> o .: key
        "number" -> NumberValue pid <$> o .: key
        "select" -> SelectValue pid <$> o .:? key
        "multi_select" -> MultiSelectValue pid <$> o .: key
        "date" -> DateValue pid <$> o .:? key
        "people" -> PeopleValue pid <$> o .: key
        "files" -> FilesValue pid <$> o .: key
        "checkbox" -> CheckboxValue pid <$> o .: key
        "url" -> UrlValue pid <$> o .:? key
        "email" -> EmailValue pid <$> o .:? key
        "phone_number" -> PhoneNumberValue pid <$> o .:? key
        "formula" -> FormulaValue pid <$> o .: key
        "relation" -> RelationValue pid <$> o .: key <*> o .:? "has_more"
        "rollup" -> RollupValue pid <$> o .: key
        "created_time" -> CreatedTimeValue pid <$> o .: key
        "created_by" -> CreatedByValue pid <$> o .: key
        "last_edited_time" -> LastEditedTimeValue pid <$> o .: key
        "last_edited_by" -> LastEditedByValue pid <$> o .: key
        "status" -> StatusValue pid <$> o .:? key
        "unique_id" -> UniqueIdValue pid <$> o .: key
        "place" -> PlaceValue pid <$> o .:? key
        "button" -> ButtonValue pid <$> o .:? key
        "verification" -> VerificationValue pid <$> o .:? key
        other -> UnknownPropertyValue pid other . fromMaybe Null <$> o .:? key
    _ -> fail "Expected object for PropertyValue"

instance ToJSON PropertyValue where
  toJSON = \case
    TitleValue _ v -> Aeson.object ["title" .= v]
    RichTextValue _ v -> Aeson.object ["rich_text" .= v]
    NumberValue _ v -> Aeson.object ["number" .= v]
    SelectValue _ v -> Aeson.object ["select" .= v]
    MultiSelectValue _ v -> Aeson.object ["multi_select" .= v]
    DateValue _ v -> Aeson.object ["date" .= v]
    PeopleValue _ v -> Aeson.object ["people" .= v]
    FilesValue _ v -> Aeson.object ["files" .= v]
    CheckboxValue _ v -> Aeson.object ["checkbox" .= v]
    UrlValue _ v -> Aeson.object ["url" .= v]
    EmailValue _ v -> Aeson.object ["email" .= v]
    PhoneNumberValue _ v -> Aeson.object ["phone_number" .= v]
    FormulaValue _ v -> Aeson.object ["formula" .= v]
    RelationValue _ v _ -> Aeson.object ["relation" .= v]
    RollupValue _ v -> Aeson.object ["rollup" .= v]
    CreatedTimeValue _ v -> Aeson.object ["created_time" .= v]
    CreatedByValue _ v -> Aeson.object ["created_by" .= v]
    LastEditedTimeValue _ v -> Aeson.object ["last_edited_time" .= v]
    LastEditedByValue _ v -> Aeson.object ["last_edited_by" .= v]
    StatusValue _ v -> Aeson.object ["status" .= v]
    UniqueIdValue _ v -> Aeson.object ["unique_id" .= v]
    PlaceValue _ v -> Aeson.object ["place" .= v]
    ButtonValue _ v -> Aeson.object ["button" .= v]
    VerificationValue _ v -> Aeson.object ["verification" .= v]
    UnknownPropertyValue _ t v -> Aeson.object [Key.fromText t .= v]

-- | Encode modeled response data, including the property's ID and type.
-- Unlike 'toJSON', which builds mutation payloads, this preserves supplied
-- relation completeness and full users, and recursively encodes rollup arrays.
-- An empty ID is the model's absent-ID sentinel and is omitted (including in
-- nested rollups). Nonempty IDs are preserved verbatim. Unmodeled response
-- fields and original JSON bytes are not retained.
propertyValueResponseJSON :: PropertyValue -> Value
propertyValueResponseJSON = \case
  TitleValue pid v -> envelope pid "title" (toJSON (fmap richTextResponseJSON v)) []
  RichTextValue pid v -> envelope pid "rich_text" (toJSON (fmap richTextResponseJSON v)) []
  NumberValue pid v -> envelope pid "number" (toJSON v) []
  SelectValue pid v -> envelope pid "select" (toJSON v) []
  MultiSelectValue pid v -> envelope pid "multi_select" (toJSON v) []
  DateValue pid v -> envelope pid "date" (toJSON v) []
  PeopleValue pid v -> envelope pid "people" (toJSON (fmap peopleEntryResponseJSON v)) []
  FilesValue pid v -> envelope pid "files" (toJSON v) []
  CheckboxValue pid v -> envelope pid "checkbox" (toJSON v) []
  UrlValue pid v -> envelope pid "url" (toJSON v) []
  EmailValue pid v -> envelope pid "email" (toJSON v) []
  PhoneNumberValue pid v -> envelope pid "phone_number" (toJSON v) []
  FormulaValue pid v -> envelope pid "formula" (toJSON v) []
  RelationValue pid v hasMore -> envelope pid "relation" (toJSON v) (optionalResponsePair "has_more" hasMore)
  RollupValue pid v -> envelope pid "rollup" (rollupResponseJSON v) []
  CreatedTimeValue pid v -> envelope pid "created_time" (toJSON v) []
  CreatedByValue pid v -> envelope pid "created_by" (toJSON v) []
  LastEditedTimeValue pid v -> envelope pid "last_edited_time" (toJSON v) []
  LastEditedByValue pid v -> envelope pid "last_edited_by" (toJSON v) []
  StatusValue pid v -> envelope pid "status" (toJSON v) []
  UniqueIdValue pid v -> envelope pid "unique_id" (toJSON v) []
  PlaceValue pid v -> envelope pid "place" (toJSON v) []
  ButtonValue pid v -> envelope pid "button" (toJSON v) []
  VerificationValue pid v -> envelope pid "verification" (maybe Null verificationResponseJSON v) []
  UnknownPropertyValue pid t v -> envelope pid t v []
  where
    envelope pid t v extra =
      Aeson.object (["id" .= pid | pid /= ""] <> ["type" .= t, Key.fromText t .= v] <> extra)

optionalResponsePair :: (ToJSON a) => Key.Key -> Maybe a -> [Pair]
optionalResponsePair key = maybe [] (\v -> [key .= v])

rollupResponseJSON :: RollupResult -> Value
rollupResponseJSON = \case
  RollupArrayResult v fn ->
    Aeson.object ["type" .= ("array" :: Text), "array" .= fmap propertyValueResponseJSON v, "function" .= fn]
  other -> toJSON other

peopleEntryResponseJSON :: PeopleEntry -> Value
peopleEntryResponseJSON = \case
  PersonEntry u -> userResponseJSON u
  group@(GroupEntry _) -> toJSON group

userResponseJSON :: UserValue -> Value
userResponseJSON = \case
  partial@(PartialUser _) -> toJSON partial
  FullUser (Users.UserObject uid uname avatar userType person bot objectType) ->
    Aeson.object $
      ["object" .= objectType, "id" .= uid, "type" .= userTypeText userType]
        <> optionalResponsePair "name" uname
        <> optionalResponsePair "avatar_url" avatar
        <> optionalResponsePair "person" (fmap personResponseJSON person)
        <> optionalResponsePair "bot" (fmap botResponseJSON bot)
  where
    userTypeText Users.Person = "person" :: Text
    userTypeText Users.Bot = "bot"
    personResponseJSON (Users.PersonUser email) = Aeson.object (optionalResponsePair "email" email)
    botResponseJSON (Users.BotUser owner workspaceName workspaceId limits) =
      Aeson.object $
        optionalResponsePair "owner" (fmap ownerResponseJSON owner)
          <> optionalResponsePair "workspace_name" workspaceName
          <> optionalResponsePair "workspace_id" workspaceId
          <> optionalResponsePair "workspace_limits" (fmap limitsResponseJSON limits)
    limitsResponseJSON (Users.WorkspaceLimits maxSize) =
      Aeson.object (optionalResponsePair "max_file_upload_size_in_bytes" maxSize)
    ownerResponseJSON = \case
      Users.UserOwner t uid -> Aeson.object ["type" .= t, "user" .= Aeson.object ["id" .= uid]]
      Users.WorkspaceOwner t workspace -> Aeson.object ["type" .= t, "workspace" .= workspace]
      Users.UnknownOwner _ raw -> raw

verificationResponseJSON :: VerificationResult -> Value
verificationResponseJSON VerificationResult {..} =
  Aeson.object $
    ["state" .= state]
      <> optionalResponsePair "date" date
      <> optionalResponsePair "verified_by" (fmap userResponseJSON verifiedBy)

-- Reuse the established rich-text envelope, replacing only the nested user.
richTextResponseJSON :: RichText -> Value
richTextResponseJSON rt@RichText {content = MentionContentWrapper (UserMention u)} =
  case toJSON rt of
    Object o -> Object (KeyMap.insert "mention" (Aeson.object ["type" .= ("user" :: Text), "user" .= userResponseJSON u]) o)
    v -> v
richTextResponseJSON rt = toJSON rt

-- ---------------------------------------------------------------------------
-- Supporting types
-- ---------------------------------------------------------------------------

-- | A select/multi-select/status option value as it appears in page properties.
--
-- Note: 'color' is @Maybe Text@ (not 'SelectColor') because the API returns
-- color names as strings in property values, and the set may differ from schema colors.
data SelectOptionValue = SelectOptionValue
  { id :: Maybe Text,
    name :: Text,
    color :: Maybe Text,
    description :: Maybe Text
  }
  deriving stock (Generic, Show)

instance FromJSON SelectOptionValue where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON SelectOptionValue where
  toJSON = genericToJSON aesonOptions

-- | A file value in a files property. Can be an internal (Notion-hosted) file
-- or an external URL.
data FileValue
  = InternalFileValue {name :: Text, file :: File}
  | ExternalFileValue {name :: Text, external :: ExternalFile}
  | FileUploadFileValue {name :: Text, fileUploadId :: UUID}
  deriving stock (Show)

instance FromJSON FileValue where
  parseJSON = \case
    Object o -> do
      fileType <- o .: "type"
      n <- o .: "name"
      case fileType of
        "file" -> InternalFileValue n <$> o .: "file"
        "external" -> ExternalFileValue n <$> o .: "external"
        "file_upload" -> do
          uploadObj <- o .: "file_upload"
          uploadId <- uploadObj .: "id"
          pure (FileUploadFileValue n uploadId)
        other -> fail $ "Unknown file value type: " <> unpack other
    _ -> fail "Expected object for FileValue"

instance ToJSON FileValue where
  toJSON (InternalFileValue n f) =
    Aeson.object ["type" .= ("file" :: Text), "name" .= n, "file" .= f]
  toJSON (ExternalFileValue n e) =
    Aeson.object ["type" .= ("external" :: Text), "name" .= n, "external" .= e]
  toJSON (FileUploadFileValue n uid) =
    Aeson.object
      [ "type" .= ("file_upload" :: Text),
        "name" .= n,
        "file_upload" .= Aeson.object ["id" .= uid]
      ]

-- | A relation reference (just a page ID).
newtype RelationRef = RelationRef {id :: UUID}
  deriving stock (Show)

instance FromJSON RelationRef where
  parseJSON = \case
    Object o -> RelationRef <$> o .: "id"
    _ -> fail "Expected object for RelationRef"

instance ToJSON RelationRef where
  toJSON (RelationRef rid) = Aeson.object ["id" .= rid]

-- | The result of a formula property (read-only).
data FormulaResult
  = FormulaStringResult (Maybe Text)
  | FormulaNumberResult (Maybe Scientific)
  | FormulaBooleanResult (Maybe Bool)
  | FormulaDateResult (Maybe Date)
  | FormulaUnsupportedResult
  | -- | A result kind this library does not model yet; holds the raw object.
    UnknownFormulaResult Value
  deriving stock (Show)

instance FromJSON FormulaResult where
  parseJSON = \case
    Object o -> do
      formulaType :: Text <- o .: "type"
      case formulaType of
        "string" -> FormulaStringResult <$> o .:? "string"
        "number" -> FormulaNumberResult <$> o .:? "number"
        "boolean" -> FormulaBooleanResult <$> o .:? "boolean"
        "date" -> FormulaDateResult <$> o .:? "date"
        "unsupported" -> pure FormulaUnsupportedResult
        _ -> pure (UnknownFormulaResult (Object o))
    _ -> fail "Expected object for FormulaResult"

instance ToJSON FormulaResult where
  toJSON = \case
    FormulaStringResult v -> Aeson.object ["type" .= ("string" :: Text), "string" .= v]
    FormulaNumberResult v -> Aeson.object ["type" .= ("number" :: Text), "number" .= v]
    FormulaBooleanResult v -> Aeson.object ["type" .= ("boolean" :: Text), "boolean" .= v]
    FormulaDateResult v -> Aeson.object ["type" .= ("date" :: Text), "date" .= v]
    FormulaUnsupportedResult -> Aeson.object ["type" .= ("unsupported" :: Text), "unsupported" .= Aeson.object []]
    UnknownFormulaResult v -> v

-- | The result of a rollup property (read-only).
data RollupResult
  = RollupNumberResult (Maybe Scientific) RollupFunction
  | RollupDateResult (Maybe Date) RollupFunction
  | -- | The rolled-up property values. They carry no property ID.
    RollupArrayResult (Vector PropertyValue) RollupFunction
  | RollupIncompleteResult RollupFunction
  | RollupUnsupportedResult RollupFunction
  | -- | A rollup result type this library does not model yet: the type name
    -- and the raw rollup object.
    RollupUnknownResult Text Value
  deriving stock (Show)

instance FromJSON RollupResult where
  parseJSON = \case
    Object o -> do
      rollupType <- o .: "type"
      case rollupType of
        "number" -> RollupNumberResult <$> o .:? "number" <*> o .: "function"
        "date" -> RollupDateResult <$> o .:? "date" <*> o .: "function"
        "array" -> do
          raw :: Vector Value <- o .: "array"
          -- The paginated property-item summary sends empty objects here.
          values <- traverse parseJSON (Vector.filter (/= Aeson.object []) raw)
          RollupArrayResult values <$> o .: "function"
        "incomplete" -> RollupIncompleteResult <$> o .: "function"
        "unsupported" -> RollupUnsupportedResult <$> o .: "function"
        other -> pure (RollupUnknownResult other (Object o))
    _ -> fail "Expected object for RollupResult"

instance ToJSON RollupResult where
  toJSON = \case
    RollupNumberResult v fn -> Aeson.object ["type" .= ("number" :: Text), "number" .= v, "function" .= fn]
    RollupDateResult v fn -> Aeson.object ["type" .= ("date" :: Text), "date" .= v, "function" .= fn]
    RollupArrayResult v fn -> Aeson.object ["type" .= ("array" :: Text), "array" .= v, "function" .= fn]
    RollupIncompleteResult fn -> Aeson.object ["type" .= ("incomplete" :: Text), "function" .= fn]
    RollupUnsupportedResult fn -> Aeson.object ["type" .= ("unsupported" :: Text), "function" .= fn]
    RollupUnknownResult _ v -> v

-- | Unique ID property value (read-only).
data UniqueIdResult = UniqueIdResult
  { number :: Maybe Natural,
    prefix :: Maybe Text
  }
  deriving stock (Generic, Show)

instance FromJSON UniqueIdResult where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON UniqueIdResult where
  toJSON = genericToJSON aesonOptions

-- | Verification property value. Build request values with 'verifiedValue'
-- and 'unverifiedValue'.
data VerificationResult = VerificationResult
  { state :: VerificationState,
    verifiedBy :: Maybe UserValue,
    date :: Maybe Date
  }
  deriving stock (Generic, Show)

instance FromJSON VerificationResult where
  parseJSON = genericParseJSON aesonOptions

-- | Always writes @state@; @date@ and @verified_by@ only when present.
instance ToJSON VerificationResult where
  toJSON VerificationResult {..} =
    Aeson.object $
      ["state" .= state]
        <> maybe [] (\d -> ["date" .= d]) date
        <> maybe [] (\u -> ["verified_by" .= u]) verifiedBy

-- | State of a verification property.
data VerificationState
  = Verified
  | Expired
  | Unverified
  | -- | A state this library does not know yet; holds the raw string.
    UnknownVerificationState Text
  deriving stock (Eq, Generic, Show)

instance FromJSON VerificationState where
  parseJSON = Aeson.withText "VerificationState" $ \case
    "verified" -> pure Verified
    "expired" -> pure Expired
    "unverified" -> pure Unverified
    other -> pure (UnknownVerificationState other)

instance ToJSON VerificationState where
  toJSON = \case
    Verified -> String "verified"
    Expired -> String "expired"
    Unverified -> String "unverified"
    UnknownVerificationState t -> String t

-- | A place property value (a location with optional name and address).
data Place = Place
  { lat :: Double,
    lon :: Double,
    name :: Maybe Text,
    address :: Maybe Text,
    awsPlaceId :: Maybe Text,
    googlePlaceId :: Maybe Text
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON Place where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON Place where
  toJSON = genericToJSON aesonOptions

-- ---------------------------------------------------------------------------
-- Smart constructors
-- ---------------------------------------------------------------------------

-- | Create a title property value.
titleValue :: Vector RichText -> PropertyValue
titleValue = TitleValue ""

-- | Create a rich text property value.
richTextValue :: Vector RichText -> PropertyValue
richTextValue = RichTextValue ""

-- | Create a number property value.
numberValue :: Scientific -> PropertyValue
numberValue n = NumberValue "" (Just n)

-- | Create a select property value by option name.
selectValue :: Text -> PropertyValue
selectValue name = SelectValue "" (Just (SelectOptionValue Nothing name Nothing Nothing))

-- | Create a multi-select property value from a list of option names.
multiSelectValue :: [Text] -> PropertyValue
multiSelectValue names = MultiSelectValue "" (Vector.fromList (map (\n -> SelectOptionValue Nothing n Nothing Nothing) names))

-- | Create a date property value.
dateValue :: Text -> Maybe Text -> PropertyValue
dateValue start end = DateValue "" (Just (Date start end Nothing))

-- | Create a checkbox property value.
checkboxValue :: Bool -> PropertyValue
checkboxValue = CheckboxValue ""

-- | Create a URL property value.
urlValue :: Text -> PropertyValue
urlValue t = UrlValue "" (Just t)

-- | Create an email property value.
emailValue :: Text -> PropertyValue
emailValue t = EmailValue "" (Just t)

-- | Create a phone number property value.
phoneNumberValue :: Text -> PropertyValue
phoneNumberValue t = PhoneNumberValue "" (Just t)

-- | Create a relation property value from a list of page IDs.
relationValue :: [UUID] -> PropertyValue
relationValue ids = RelationValue "" (Vector.fromList (map RelationRef ids)) Nothing

-- | Create a status property value by option name.
statusValue :: Text -> PropertyValue
statusValue name = StatusValue "" (Just (SelectOptionValue Nothing name Nothing Nothing))

-- | Create a people property value from a list of user IDs.
peopleValue :: [UUID] -> PropertyValue
peopleValue ids = PeopleValue "" (Vector.fromList (map (PersonEntry . PartialUser) ids))

-- | Create a people property value from users and groups.
peopleEntriesValue :: [PeopleEntry] -> PropertyValue
peopleEntriesValue = PeopleValue "" . Vector.fromList

-- | Create a place property value from a latitude and longitude.
placeValue :: Double -> Double -> PropertyValue
placeValue latitude longitude =
  PlaceValue "" (Just (Place latitude longitude Nothing Nothing Nothing Nothing))

-- | Mark a verification property as verified, optionally until a date.
verifiedValue :: Maybe Date -> PropertyValue
verifiedValue d = VerificationValue "" (Just (VerificationResult Verified Nothing d))

-- | Mark a verification property as unverified.
unverifiedValue :: PropertyValue
unverifiedValue = VerificationValue "" (Just (VerificationResult Unverified Nothing Nothing))

-- | Create a files property value from a list of external URLs.
filesValue :: [Text] -> PropertyValue
filesValue urls = FilesValue "" (Vector.fromList (map (\u -> ExternalFileValue "" (ExternalFile u)) urls))

-- | Create a files property value from a list of file upload references.
fileUploadFilesValue :: [(Text, UUID)] -> PropertyValue
fileUploadFilesValue pairs = FilesValue "" (Vector.fromList (map (\(n, uid) -> FileUploadFileValue n uid) pairs))