Refactor: use types derived from API schema

addresses #2366 #2371 #2381 #2386 #2391

closes #2388

Co-Authored-By: ArneBo <arne@ecobasa.org>
Co-Authored-By: Flupsi <upsiflu@gmail.com>
Co-Authored-By: jon r <jon@allmende.io>
This commit is contained in:
jon r 2025-04-18 10:57:34 +02:00
parent 5997a0f0bb
commit 3e9d75d089
6 changed files with 23411 additions and 221 deletions

View File

@ -16,10 +16,13 @@
"test": "vitest run", "test": "vitest run",
"test:unit": "vitest run --coverage", "test:unit": "vitest run --coverage",
"test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node", "test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node",
"lint": "eslint --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html src test cypress public/embed.html", "lint": "yarn lint:es && yarn lint:tsc",
"lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress", "lint:es": "eslint --max-warnings 0 --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html,.cjs . cypress public/embed.html src test ui-docs",
"fix-fomantic-css": "scripts/fix-fomantic-css.sh", "lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental --project tsconfig.json",
"postinstall": "yarn run fix-fomantic-css" "generate-types-from-local-schema": "yarn run openapi-typescript ../api/funkwhale_api/common/schema.yml -o src/generated/types.ts",
"generate-types-from-remote-schema": "yarn run openapi-typescript https://docs.funkwhale.audio/develop/swagger/schema.yml -o src/generated/types.ts",
"fmt:es": "yarn lint:es --fix",
"fmt:html": "node --experimental-strip-types node_modules/prettier/bin/prettier.cjs index.html public/embed.html --write"
}, },
"dependencies": { "dependencies": {
"@sentry/tracing": "7.47.0", "@sentry/tracing": "7.47.0",

View File

@ -0,0 +1 @@
../../../api/funkwhale_api/common/schema.yml

23350
front/src/generated/types.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
import type { InitModule } from '~/types' import type { InitModule } from '~/types'
import { setupDropdown } from '~/utils/fomantic'
export const install: InitModule = ({ app, store }) => { export const install: InitModule = ({ app, store }) => {
app.directive('title', function (el, binding) { app.directive('title', function (el, binding) {
store.commit('ui/pageTitle', binding.value) store.commit('ui/pageTitle', binding.value)
}) })
app.directive('dropdown', (element) => setupDropdown(element))
} }

View File

@ -1,4 +1,4 @@
import type { NodeInfo } from '~/store/instance' import type { components } from '~/generated/types'
import type { InitModule } from '~/types' import type { InitModule } from '~/types'
import { whenever } from '@vueuse/core' import { whenever } from '@vueuse/core'
@ -15,7 +15,7 @@ export const install: InitModule = async ({ store, router }) => {
const fetchNodeInfo = async () => { const fetchNodeInfo = async () => {
try { try {
const [{ data }] = await Promise.all([ const [{ data }] = await Promise.all([
axios.get<NodeInfo>('instance/nodeinfo/2.1/'), axios.get<components['schemas']['NodeInfo21']>('instance/nodeinfo/2.1/'),
store.dispatch('instance/fetchSettings') store.dispatch('instance/fetchSettings')
]) ])

View File

@ -4,6 +4,9 @@ import type { Router } from 'vue-router'
import type { AxiosError } from 'axios' import type { AxiosError } from 'axios'
import type { RootState } from '~/store' import type { RootState } from '~/store'
// App types are synced from the backend. Run `yarn update-schema`.
import { type components } from '~/generated/types.ts'
// eslint-disable-next-line // eslint-disable-next-line
import type { ComponentPublicInstance } from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { QueueTrack } from '~/composables/audio/queue' import type { QueueTrack } from '~/composables/audio/queue'
@ -41,190 +44,46 @@ export interface ThemeEntry {
// Track stuff // Track stuff
export type ContentCategory = 'podcast' | 'music' export type ContentCategory = 'podcast' | 'music'
export interface Artist { // Use backend-defined schema types
id: number
fid: string
mbid?: string
name: string export type Actor = components['schemas']['FullActor']
description: Content export type Activity = components['schemas']['Activity']
cover?: Cover export type Album = components['schemas']['Album']
channel?: Channel export type ArtistCredit = components['schemas']['ArtistCredit']
// TODO (wvffle): Check if it's Tag[] or string[] export type Channel = components['schemas']['Channel']
tags: string[] export type Library = components['schemas']['Library']
export type License = components['schemas']['License']
export type Listening = components['schemas']['Listening']
export type Playlist = components['schemas']['Playlist']
export type PlaylistTrack = components['schemas']['PlaylistTrack']
export type PrivacyLevelEnum = components['schemas']['PrivacyLevelEnum']
export type Radio = components['schemas']['Radio']
export type SearchResult = components['schemas']['SearchResult']
export type Tag = components['schemas']['Tag']
export type Track = components['schemas']['Track']
export type Usage = components['schemas']['Usage']
export type LibraryScan = components['schemas']['LibraryScan']
export type LibraryFollow = components['schemas']['LibraryFollow']
export type Cover = components['schemas']['CoverField']
export type RateLimitStatus = components['schemas']['RateLimit']['scopes'][number]
export type PaginatedAlbumList = components['schemas']['PaginatedAlbumList']
export type PaginatedChannelList = components['schemas']['PaginatedChannelList']
content_category: ContentCategory export type Artist = components['schemas']['Artist']
albums: Album[]
tracks_count: number
attributed_to: Actor
is_local: boolean
is_playable: boolean
modification_date?: string
}
export interface ArtistCredit { export type PrivacyLevel = components['schemas']['LibraryPrivacyLevelEnum']
artist: Artist
credit: string
joinphrase: string
index: number
}
export interface Album { export type ImportStatus = components['schemas']['ImportStatusEnum']
id: number
fid: string
mbid?: string
title: string // TODO: Find out which type: `Follow` or `LibraryFollow`
description: Content // export interface UserFollow {
release_date?: string // uuid: string
cover?: Cover // approved: boolean
tags: string[]
artist_credit: ArtistCredit[] // name: string
tracks_count: number // type?: 'federation.Actor' | 'federation.UserFollow'
tracks: Track[] // target?: Actor
// }
is_playable: boolean
is_local: boolean
}
export interface Track {
id: number
fid: string
mbid?: string
title: string
description: Content
cover?: Cover
position?: number
copyright?: string
license?: License
tags: string[]
uploads: Upload[]
downloads_count: number
album?: Album
artist_credit: ArtistCredit[]
disc_number: number
listen_url: string
creation_date: string
attributed_to: Actor
is_playable: boolean
is_local: boolean
}
export interface Channel {
id: number
uuid: string
artist?: Artist
actor: Actor
attributed_to: Actor
url?: string
rss_url: string
subscriptions_count: number
downloads_count: number
content_category: ContentCategory
metadata?: {
itunes_category?: unknown
itunes_subcategory?: unknown
language?: string
owner_name?: string
owner_email?: string
}
}
export type PrivacyLevel = 'everyone' | 'instance' | 'me'
export interface Library {
id: number
uuid: string
fid?: string
name: string
actor: Actor
uploads_count: number
size: number
description: string
privacy_level: PrivacyLevel
creation_date: string
follow?: LibraryFollow
latest_scan: LibraryScan
}
export type ImportStatus = 'scanning' | 'pending' | 'finished' | 'errored' | 'draft' | 'skipped'
export interface LibraryScan {
processed_files: number
total_files: number
status: ImportStatus
errored_files: number
modification_date: string
}
export interface LibraryFollow {
uuid: string
approved: boolean
name: string
type?: 'music.Library' | 'federation.LibraryFollow'
target: Library
}
export interface UserFollow {
uuid: string
approved: boolean
name: string
type?: 'federation.Actor' | 'federation.UserFollow'
target?: Actor
}
export interface Cover {
uuid: string
urls: {
original: string
medium_square_crop: string
large_square_crop: string
}
}
export interface License {
code: string
name: string
url: string
}
export interface Playlist {
id: number
name: string
modification_date: string
actor: Actor
privacy_level: PrivacyLevel
tracks_count: number
duration: number
album_covers: string[]
is_playable: boolean
}
export interface PlaylistTrack {
track: Track
position?: number
}
export interface Radio {
id: number
name: string
user: User
}
export interface Listening {
id: number
track: Track
user: User
actor: Actor
creation_date: string
}
// API stuff // API stuff
// eslint-disable-next-line // eslint-disable-next-line
@ -236,21 +95,14 @@ export interface BackendError extends AxiosError {
rawPayload?: APIErrorResponse rawPayload?: APIErrorResponse
} }
// Backend response now contains pagination fields.
// Example: PaginatedArtistWithAlbumsList
// Example: PaginatedAlbumsList
export interface BackendResponse<T> { export interface BackendResponse<T> {
count: number count: number
results: T[] results: T[]
} }
export interface RateLimitStatus {
limit?: string
scope?: string
remaining?: string
duration?: string
availableSeconds: number
reset?: string
resetSeconds?: string
}
// WebSocket stuff // WebSocket stuff
// FS Browser // FS Browser
@ -312,17 +164,17 @@ export interface Upload {
} }
// Profile stuff // Profile stuff
export interface Actor { // export interface Actor {
id: number // id: number
fid?: string // fid?: string
name?: string // name?: string
icon?: Cover // icon?: Cover
summary: string // summary: string
preferred_username: string // preferred_username: string
full_username: string // full_username: string
is_local: boolean // is_local: boolean
domain: string // domain: string
} // }
export interface User { export interface User {
id: number id: number
@ -486,24 +338,12 @@ export interface UserRequest {
} }
// Notification stuff // Notification stuff
export type Activity = {
actor: Actor
creation_date: string
related_object: LibraryFollow | UserFollow
type: 'Follow' | 'Accept'
object: LibraryFollow | UserFollow
}
export interface Notification { export interface Notification {
id: number id: number
is_read: boolean is_read: boolean
activity: Activity activity: Activity
} }
// Tags stuff
export interface Tag {
name: string
}
// Application stuff // Application stuff
export interface Application { export interface Application {
client_id: string client_id: string