project-standalo-sonic-cloud/.workflow/versions/v003/design/design_document.yml

703 lines
20 KiB
YAML

workflow_version: "v003"
feature: "add label management system"
created_at: "2025-12-18T17:45:00"
updated_at: "2025-12-18T17:50:00"
approved_at: null
status: draft
revision: 2
revision_notes: "Updated to remove external model references (existing models in codebase)"
# Note: This design extends existing models (Label, Artist, Song, Album, User)
# which are already defined in prisma/schema.prisma
# The dependencies on these models are implicit (referenced by foreign key field names)
# ============================================================================
# LAYER 1: DATA MODELS
# ============================================================================
data_models:
- id: model_label_invitation
name: LabelInvitation
description: "Invitations from labels to artists to join their roster"
table_name: label_invitations
fields:
- name: id
type: uuid
constraints: [primary_key]
description: "Unique identifier"
- name: labelId
type: uuid
constraints: [foreign_key, not_null, indexed]
description: "Reference to the inviting label (FK to labels table)"
- name: artistId
type: uuid
constraints: [foreign_key, not_null, indexed]
description: "Reference to the invited artist (FK to artists table)"
- name: status
type: enum
enum_values: [pending, accepted, declined, expired]
constraints: [not_null, default]
default: pending
description: "Current status of the invitation"
- name: message
type: text
constraints: []
description: "Optional message from label to artist"
- name: expiresAt
type: datetime
constraints: [not_null]
description: "When the invitation expires"
- name: createdAt
type: datetime
constraints: [not_null]
description: "When invitation was created"
- name: updatedAt
type: datetime
constraints: [not_null]
description: "When invitation was last updated"
relations: [] # Relations are to existing models, handled by FK fields
indexes:
- fields: [labelId, artistId]
unique: true
name: label_artist_unique
- fields: [artistId, status]
unique: false
name: artist_pending_invitations
timestamps: true
soft_delete: false
validations:
- field: expiresAt
rule: "future_date"
message: "Expiration date must be in the future"
# ============================================================================
# LAYER 2: API ENDPOINTS
# ============================================================================
api_endpoints:
# === LABEL CRUD ===
- id: api_get_label
method: GET
path: /api/labels/[id]
summary: "Get label details"
description: "Retrieve label profile with artist roster and statistics"
tags: [labels]
path_params:
- name: id
type: string
description: "Label ID"
responses:
- status: 200
description: "Label found"
schema:
type: object
properties:
- name: id
type: uuid
- name: name
type: string
- name: slug
type: string
- name: description
type: string
- name: logoUrl
type: string
- name: website
type: string
- name: artists
type: array
- name: _count
type: object
example:
id: "550e8400-e29b-41d4-a716-446655440000"
name: "Sonic Records"
slug: "sonic-records"
description: "Independent music label"
artists: []
_count: { artists: 5, songs: 120 }
- status: 404
description: "Label not found"
schema:
type: object
properties:
- name: error
type: string
depends_on_models: [model_label_invitation] # New model only
depends_on_apis: []
auth:
required: false
roles: []
- id: api_update_label
method: PUT
path: /api/labels/[id]
summary: "Update label profile"
description: "Update label name, description, logo, and website"
tags: [labels]
path_params:
- name: id
type: string
description: "Label ID"
request_body:
content_type: application/json
schema:
type: object
properties:
- name: name
type: string
required: false
description: "Label name"
- name: description
type: string
required: false
description: "Label description"
- name: logoUrl
type: string
required: false
description: "Logo URL"
- name: website
type: string
required: false
description: "Website URL"
responses:
- status: 200
description: "Label updated"
schema:
type: object
properties:
- name: id
type: uuid
- name: name
type: string
- name: slug
type: string
- name: description
type: string
- status: 401
description: "Unauthorized"
- status: 403
description: "Not label owner"
- status: 404
description: "Label not found"
depends_on_models: []
depends_on_apis: []
auth:
required: true
roles: [label]
# === LABEL STATISTICS ===
- id: api_get_label_stats
method: GET
path: /api/labels/[id]/stats
summary: "Get label statistics"
description: "Get artist count, song count, album count, and total plays"
tags: [labels, statistics]
path_params:
- name: id
type: string
description: "Label ID"
responses:
- status: 200
description: "Statistics retrieved"
schema:
type: object
properties:
- name: artistCount
type: integer
- name: songCount
type: integer
- name: albumCount
type: integer
- name: totalPlays
type: integer
example:
artistCount: 5
songCount: 120
albumCount: 15
totalPlays: 45000
- status: 404
description: "Label not found"
depends_on_models: []
depends_on_apis: []
auth:
required: false
roles: []
# === LABEL INVITATIONS ===
- id: api_list_label_invitations
method: GET
path: /api/labels/[id]/invitations
summary: "List label invitations"
description: "Get all invitations sent by this label"
tags: [labels, invitations]
path_params:
- name: id
type: string
description: "Label ID"
query_params:
- name: status
type: string
required: false
description: "Filter by status (pending, accepted, declined, expired)"
responses:
- status: 200
description: "Invitations list"
schema:
type: array
properties:
- name: id
type: uuid
- name: artist
type: object
- name: status
type: string
- name: message
type: string
- name: expiresAt
type: datetime
- status: 401
description: "Unauthorized"
- status: 403
description: "Not label owner"
depends_on_models: [model_label_invitation]
depends_on_apis: []
auth:
required: true
roles: [label]
- id: api_create_label_invitation
method: POST
path: /api/labels/[id]/invitations
summary: "Create artist invitation"
description: "Send invitation to an artist to join the label"
tags: [labels, invitations]
path_params:
- name: id
type: string
description: "Label ID"
request_body:
content_type: application/json
schema:
type: object
properties:
- name: artistId
type: string
required: true
description: "ID of artist to invite"
- name: message
type: string
required: false
description: "Optional invitation message"
responses:
- status: 201
description: "Invitation created"
schema:
type: object
properties:
- name: id
type: uuid
- name: artistId
type: uuid
- name: status
type: string
- name: expiresAt
type: datetime
- status: 400
description: "Artist already has label or pending invitation"
- status: 401
description: "Unauthorized"
- status: 403
description: "Not label owner"
- status: 404
description: "Artist not found"
depends_on_models: [model_label_invitation]
depends_on_apis: []
auth:
required: true
roles: [label]
- id: api_delete_label_invitation
method: DELETE
path: /api/labels/[id]/invitations/[invitationId]
summary: "Cancel invitation"
description: "Cancel a pending invitation"
tags: [labels, invitations]
path_params:
- name: id
type: string
description: "Label ID"
- name: invitationId
type: string
description: "Invitation ID"
responses:
- status: 200
description: "Invitation cancelled"
- status: 401
description: "Unauthorized"
- status: 403
description: "Not label owner"
- status: 404
description: "Invitation not found"
depends_on_models: [model_label_invitation]
depends_on_apis: []
auth:
required: true
roles: [label]
# === ARTIST INVITATION HANDLING ===
- id: api_list_artist_invitations
method: GET
path: /api/artists/[id]/invitations
summary: "List artist invitations"
description: "Get all invitations received by this artist"
tags: [artists, invitations]
path_params:
- name: id
type: string
description: "Artist ID"
responses:
- status: 200
description: "Invitations list"
schema:
type: array
properties:
- name: id
type: uuid
- name: label
type: object
- name: status
type: string
- name: message
type: string
- name: expiresAt
type: datetime
- status: 401
description: "Unauthorized"
- status: 403
description: "Not the artist"
depends_on_models: [model_label_invitation]
depends_on_apis: []
auth:
required: true
roles: [artist]
- id: api_respond_to_invitation
method: POST
path: /api/artists/[id]/invitations/[invitationId]/respond
summary: "Respond to invitation"
description: "Accept or decline a label invitation"
tags: [artists, invitations]
path_params:
- name: id
type: string
description: "Artist ID"
- name: invitationId
type: string
description: "Invitation ID"
request_body:
content_type: application/json
schema:
type: object
properties:
- name: response
type: string
required: true
description: "Accept or decline (accept/decline)"
responses:
- status: 200
description: "Response recorded"
schema:
type: object
properties:
- name: status
type: string
- name: label
type: object
- status: 400
description: "Invalid response or invitation already processed"
- status: 401
description: "Unauthorized"
- status: 403
description: "Not the invited artist"
- status: 404
description: "Invitation not found"
depends_on_models: [model_label_invitation]
depends_on_apis: []
auth:
required: true
roles: [artist]
# === ARTIST MANAGEMENT ===
- id: api_remove_artist_from_label
method: DELETE
path: /api/labels/[id]/artists/[artistId]
summary: "Remove artist from label"
description: "Remove an artist from the label roster"
tags: [labels, artists]
path_params:
- name: id
type: string
description: "Label ID"
- name: artistId
type: string
description: "Artist ID"
responses:
- status: 200
description: "Artist removed from label"
- status: 401
description: "Unauthorized"
- status: 403
description: "Not label owner"
- status: 404
description: "Artist not in label"
depends_on_models: []
depends_on_apis: []
auth:
required: true
roles: [label]
# ============================================================================
# LAYER 3: UI PAGES
# ============================================================================
pages:
- id: page_label_profile
name: "Label Profile"
path: /label/[id]
layout: layout_main
data_needs:
- api_id: api_get_label
purpose: "Display label info and artist roster"
on_load: true
- api_id: api_get_label_stats
purpose: "Display label statistics"
on_load: true
components:
- component_label_header
- component_label_stats
- component_artist_roster
seo:
title: "{{label.name}} | Sonic Cloud"
description: "{{label.description}}"
auth:
required: false
roles: []
redirect: null
- id: page_label_dashboard
name: "Label Dashboard"
path: /label/dashboard
layout: layout_main
data_needs:
- api_id: api_get_label
purpose: "Display label info"
on_load: true
- api_id: api_get_label_stats
purpose: "Display statistics"
on_load: true
- api_id: api_list_label_invitations
purpose: "Show pending invitations"
on_load: true
components:
- component_label_stats
- component_artist_roster
- component_invitation_card
- component_invite_artist_modal
seo:
title: "Label Dashboard | Sonic Cloud"
description: "Manage your label"
auth:
required: true
roles: [label]
redirect: /login
- id: page_label_settings
name: "Label Settings"
path: /label/settings
layout: layout_main
data_needs:
- api_id: api_get_label
purpose: "Load current label data for editing"
on_load: true
components:
- component_label_profile_form
seo:
title: "Label Settings | Sonic Cloud"
description: "Edit your label profile"
auth:
required: true
roles: [label]
redirect: /login
- id: page_label_invitations
name: "Label Invitations"
path: /label/invitations
layout: layout_main
data_needs:
- api_id: api_list_label_invitations
purpose: "List all invitations"
on_load: true
components:
- component_invitation_card
- component_invite_artist_modal
seo:
title: "Invitations | Sonic Cloud"
description: "Manage artist invitations"
auth:
required: true
roles: [label]
redirect: /login
# ============================================================================
# LAYER 3: UI COMPONENTS
# ============================================================================
components:
- id: component_label_card
name: LabelCard
props:
- name: label
type: "Label"
required: true
description: "Label object to display"
- name: showArtistCount
type: boolean
required: false
default: true
description: "Show number of artists"
events:
- name: onClick
payload: "string"
description: "Fires when card clicked, payload is label ID"
uses_apis: []
uses_components: []
internal_state: []
variants: [default, compact]
- id: component_label_header
name: LabelHeader
props:
- name: label
type: "Label"
required: true
description: "Label with full details"
- name: isOwner
type: boolean
required: false
default: false
description: "Is current user the label owner"
events:
- name: onEdit
payload: "void"
description: "Fires when edit button clicked"
uses_apis: []
uses_components: []
internal_state: []
variants: [default]
- id: component_label_stats
name: LabelStats
props:
- name: stats
type: "LabelStats"
required: true
description: "Statistics object"
events: []
uses_apis: []
uses_components: []
internal_state: []
variants: [default, compact]
- id: component_artist_roster
name: ArtistRoster
props:
- name: artists
type: "Artist[]"
required: true
description: "List of signed artists"
- name: isOwner
type: boolean
required: false
default: false
description: "Show management controls"
- name: emptyMessage
type: string
required: false
default: "No artists signed yet"
description: "Message when roster is empty"
events:
- name: onRemoveArtist
payload: "string"
description: "Fires when remove clicked, payload is artist ID"
- name: onArtistClick
payload: "string"
description: "Fires when artist clicked"
uses_apis: []
uses_components: [] # Uses existing ArtistCard component
internal_state: [removingArtistId]
variants: [grid, list]
- id: component_invitation_card
name: InvitationCard
props:
- name: invitation
type: "LabelInvitation"
required: true
description: "Invitation object"
- name: viewType
type: string
required: true
description: "Are we viewing as label or artist (label/artist)"
events:
- name: onAccept
payload: "string"
description: "Fires when accept clicked (artist view)"
- name: onDecline
payload: "string"
description: "Fires when decline clicked (artist view)"
- name: onCancel
payload: "string"
description: "Fires when cancel clicked (label view)"
uses_apis: []
uses_components: []
internal_state: [isProcessing]
variants: [default]
- id: component_invite_artist_modal
name: InviteArtistModal
props:
- name: isOpen
type: boolean
required: true
description: "Whether modal is open"
- name: labelId
type: string
required: true
description: "Label ID sending invitation"
events:
- name: onClose
payload: "void"
description: "Fires when modal should close"
- name: onInviteSent
payload: "LabelInvitation"
description: "Fires when invitation successfully sent"
uses_apis: [api_create_label_invitation]
uses_components: []
internal_state: [searchQuery, selectedArtist, message, isSubmitting]
variants: [default]
- id: component_label_profile_form
name: LabelProfileForm
props:
- name: label
type: "Label"
required: true
description: "Current label data"
events:
- name: onSave
payload: "Label"
description: "Fires when form saved successfully"
- name: onCancel
payload: "void"
description: "Fires when cancel clicked"
uses_apis: [api_update_label]
uses_components: []
internal_state: [formData, isSubmitting, errors]
variants: [default]