You've already forked AstralRinth
forked from didirus/AstralRinth
feat: base api-client impl (#4694)
* feat: base api-client impl * fix: doc * feat: start work on module stuff * feat: migrate v2/v3 projects into module system * fix: lint & README.md contributing * refactor: remove utils old api client prototype * fix: lint * fix: api url issues * fix: baseurl in error.vue * fix: readme * fix typo in readme * Update apps/frontend/src/providers/api-client.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Calum H. <hendersoncal117@gmail.com> * Update packages/api-client/src/features/verbose-logging.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Calum H. <hendersoncal117@gmail.com> * Update packages/api-client/src/features/retry.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Calum H. <hendersoncal117@gmail.com> --------- Signed-off-by: Calum H. <hendersoncal117@gmail.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
0
packages/api-client/src/modules/archon/.gitkeep
Normal file
0
packages/api-client/src/modules/archon/.gitkeep
Normal file
106
packages/api-client/src/modules/index.ts
Normal file
106
packages/api-client/src/modules/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { AbstractModrinthClient } from '../core/abstract-client'
|
||||
import type { AbstractModule } from '../core/abstract-module'
|
||||
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
|
||||
import { LabrinthProjectsV3Module } from './labrinth/projects/v3'
|
||||
|
||||
type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
|
||||
|
||||
/**
|
||||
* To add a new module:
|
||||
* 1. Create your module class extending AbstractModule
|
||||
* 2. Add one line here: `<api>_<module>: YourModuleClass`
|
||||
*
|
||||
* TypeScript will automatically infer the client's field structure from this registry.
|
||||
*
|
||||
* TODO: Better way? Probably not
|
||||
*/
|
||||
export const MODULE_REGISTRY = {
|
||||
labrinth_projects_v2: LabrinthProjectsV2Module,
|
||||
labrinth_projects_v3: LabrinthProjectsV3Module,
|
||||
} as const satisfies Record<string, ModuleConstructor>
|
||||
|
||||
export type ModuleID = keyof typeof MODULE_REGISTRY
|
||||
|
||||
/**
|
||||
* Parse a module ID into [api, moduleName] tuple
|
||||
*
|
||||
* @param id - Module ID in format `<api>_<module>` (e.g., 'labrinth_projects_v2')
|
||||
* @returns Tuple of [api, moduleName] (e.g., ['labrinth', 'projects_v2'])
|
||||
* @throws Error if module ID doesn't match expected format
|
||||
*/
|
||||
export function parseModuleID(id: string): [string, string] {
|
||||
const parts = id.split('_')
|
||||
if (parts.length < 2) {
|
||||
throw new Error(
|
||||
`Invalid module ID "${id}". Expected format: <api>_<module> (e.g., "labrinth_projects_v2")`,
|
||||
)
|
||||
}
|
||||
const api = parts[0]
|
||||
const moduleName = parts.slice(1).join('_')
|
||||
return [api, moduleName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build nested module structure from flat registry
|
||||
*
|
||||
* Transforms:
|
||||
* ```
|
||||
* { labrinth_projects_v2: Constructor, labrinth_users_v2: Constructor }
|
||||
* ```
|
||||
* Into:
|
||||
* ```
|
||||
* { labrinth: { projects_v2: Constructor, users_v2: Constructor } }
|
||||
* ```
|
||||
*
|
||||
* @returns Nested structure organized by API namespace
|
||||
*/
|
||||
export function buildModuleStructure(): Record<string, Record<string, ModuleConstructor>> {
|
||||
const structure: Record<string, Record<string, ModuleConstructor>> = {}
|
||||
|
||||
for (const [id, constructor] of Object.entries(MODULE_REGISTRY)) {
|
||||
const [api, moduleName] = parseModuleID(id)
|
||||
|
||||
if (!structure[api]) {
|
||||
structure[api] = {}
|
||||
}
|
||||
|
||||
structure[api][moduleName] = constructor
|
||||
}
|
||||
|
||||
return structure
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract API name from module ID
|
||||
* @example ParseAPI<'labrinth_projects_v2'> = 'labrinth'
|
||||
*/
|
||||
type ParseAPI<T extends string> = T extends `${infer API}_${string}` ? API : never
|
||||
|
||||
/**
|
||||
* Extract module name for a given API
|
||||
* @example ParseModule<'labrinth_projects_v2', 'labrinth'> = 'projects_v2'
|
||||
*/
|
||||
type ParseModule<T extends string, API extends string> = T extends `${API}_${infer Module}`
|
||||
? Module
|
||||
: never
|
||||
|
||||
/**
|
||||
* Group registry modules by API namespace
|
||||
*
|
||||
* Transforms flat registry into nested structure at the type level:
|
||||
* ```
|
||||
* { labrinth_projects_v2: ModuleClass } → { labrinth: { projects_v2: ModuleInstance } }
|
||||
* ```
|
||||
*/
|
||||
type GroupByAPI<Registry extends Record<string, ModuleConstructor>> = {
|
||||
[API in ParseAPI<keyof Registry & string>]: {
|
||||
[Module in ParseModule<keyof Registry & string, API>]: InstanceType<
|
||||
Registry[`${API}_${Module}`]
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inferred client module structure
|
||||
**/
|
||||
export type InferredClientModules = GroupByAPI<typeof MODULE_REGISTRY>
|
||||
0
packages/api-client/src/modules/kyros/.gitkeep
Normal file
0
packages/api-client/src/modules/kyros/.gitkeep
Normal file
2
packages/api-client/src/modules/labrinth/index.ts
Normal file
2
packages/api-client/src/modules/labrinth/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './projects/v2'
|
||||
export * from './projects/v3'
|
||||
115
packages/api-client/src/modules/labrinth/projects/types/v2.ts
Normal file
115
packages/api-client/src/modules/labrinth/projects/types/v2.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
export type Environment = 'required' | 'optional' | 'unsupported' | 'unknown'
|
||||
|
||||
export type ProjectStatus =
|
||||
| 'approved'
|
||||
| 'archived'
|
||||
| 'rejected'
|
||||
| 'draft'
|
||||
| 'unlisted'
|
||||
| 'processing'
|
||||
| 'withheld'
|
||||
| 'scheduled'
|
||||
| 'private'
|
||||
| 'unknown'
|
||||
|
||||
export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized'
|
||||
|
||||
export type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' | 'plugin' | 'datapack'
|
||||
|
||||
export type GalleryImageV2 = {
|
||||
url: string
|
||||
featured: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
created: string
|
||||
ordering: number
|
||||
}
|
||||
|
||||
export type DonationLinkV2 = {
|
||||
id: string
|
||||
platform: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type ProjectV2 = {
|
||||
id: string
|
||||
slug: string
|
||||
project_type: ProjectType
|
||||
team: string
|
||||
title: string
|
||||
description: string
|
||||
body: string
|
||||
published: string
|
||||
updated: string
|
||||
approved?: string
|
||||
queued?: string
|
||||
status: ProjectStatus
|
||||
requested_status?: ProjectStatus
|
||||
moderator_message?: {
|
||||
message: string
|
||||
body?: string
|
||||
}
|
||||
license: {
|
||||
id: string
|
||||
name: string
|
||||
url?: string
|
||||
}
|
||||
client_side: Environment
|
||||
server_side: Environment
|
||||
downloads: number
|
||||
followers: number
|
||||
categories: string[]
|
||||
additional_categories: string[]
|
||||
game_versions: string[]
|
||||
loaders: string[]
|
||||
versions: string[]
|
||||
icon_url?: string
|
||||
issues_url?: string
|
||||
source_url?: string
|
||||
wiki_url?: string
|
||||
discord_url?: string
|
||||
donation_urls?: DonationLinkV2[]
|
||||
gallery?: GalleryImageV2[]
|
||||
color?: number
|
||||
thread_id: string
|
||||
monetization_status: MonetizationStatus
|
||||
}
|
||||
|
||||
export type SearchResultHit = {
|
||||
project_id: string
|
||||
project_type: ProjectType
|
||||
slug: string
|
||||
author: string
|
||||
title: string
|
||||
description: string
|
||||
categories: string[]
|
||||
display_categories: string[]
|
||||
versions: string[]
|
||||
downloads: number
|
||||
follows: number
|
||||
icon_url: string
|
||||
date_created: string
|
||||
date_modified: string
|
||||
latest_version?: string
|
||||
license: string
|
||||
client_side: Environment
|
||||
server_side: Environment
|
||||
gallery: string[]
|
||||
color?: number
|
||||
}
|
||||
|
||||
export type SearchResult = {
|
||||
hits: SearchResultHit[]
|
||||
offset: number
|
||||
limit: number
|
||||
total_hits: number
|
||||
}
|
||||
|
||||
export type ProjectSearchParams = {
|
||||
query?: string
|
||||
facets?: string[][]
|
||||
filters?: string
|
||||
index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated'
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { MonetizationStatus, ProjectStatus } from './v2'
|
||||
|
||||
export type GalleryItemV3 = {
|
||||
url: string
|
||||
raw_url: string
|
||||
featured: boolean
|
||||
name?: string
|
||||
description?: string
|
||||
created: string
|
||||
ordering: number
|
||||
}
|
||||
|
||||
export type LinkV3 = {
|
||||
platform: string
|
||||
donation: boolean
|
||||
url: string
|
||||
}
|
||||
|
||||
export type ProjectV3 = {
|
||||
id: string
|
||||
slug?: string
|
||||
project_types: string[]
|
||||
games: string[]
|
||||
team_id: string
|
||||
organization?: string
|
||||
name: string
|
||||
summary: string
|
||||
description: string
|
||||
published: string
|
||||
updated: string
|
||||
approved?: string
|
||||
queued?: string
|
||||
status: ProjectStatus
|
||||
requested_status?: ProjectStatus
|
||||
license: {
|
||||
id: string
|
||||
name: string
|
||||
url?: string
|
||||
}
|
||||
downloads: number
|
||||
followers: number
|
||||
categories: string[]
|
||||
additional_categories: string[]
|
||||
loaders: string[]
|
||||
versions: string[]
|
||||
icon_url?: string
|
||||
link_urls: Record<string, LinkV3>
|
||||
gallery: GalleryItemV3[]
|
||||
color?: number
|
||||
thread_id: string
|
||||
monetization_status: MonetizationStatus
|
||||
side_types_migration_review_status: 'reviewed' | 'pending'
|
||||
[key: string]: unknown
|
||||
}
|
||||
112
packages/api-client/src/modules/labrinth/projects/v2.ts
Normal file
112
packages/api-client/src/modules/labrinth/projects/v2.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { ProjectSearchParams, ProjectV2, SearchResult } from './types/v2'
|
||||
|
||||
export class LabrinthProjectsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_projects_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project by ID or slug
|
||||
*
|
||||
* @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI')
|
||||
* @returns Promise resolving to the project data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const project = await client.labrinth.projects_v2.get('sodium')
|
||||
* console.log(project.title) // "Sodium"
|
||||
* ```
|
||||
*/
|
||||
public async get(id: string): Promise<ProjectV2> {
|
||||
return this.client.request<ProjectV2>(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple projects by IDs
|
||||
*
|
||||
* @param ids - Array of project IDs or slugs
|
||||
* @returns Promise resolving to array of projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.projects_v2.getMultiple(['sodium', 'lithium', 'phosphor'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<ProjectV2[]> {
|
||||
return this.client.request<ProjectV2[]>(`/projects`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: { ids: JSON.stringify(ids) },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Search projects
|
||||
*
|
||||
* @param params - Search parameters (query, facets, filters, etc.)
|
||||
* @returns Promise resolving to search results
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await client.labrinth.projects_v2.search({
|
||||
* query: 'optimization',
|
||||
* facets: [['categories:optimization'], ['project_type:mod']],
|
||||
* limit: 20
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async search(params: ProjectSearchParams): Promise<SearchResult> {
|
||||
return this.client.request<SearchResult>(`/search`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: params as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param data - Project update data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.edit('sodium', {
|
||||
* description: 'Updated description'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async edit(id: string, data: Partial<ProjectV2>): Promise<void> {
|
||||
return this.client.request(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.delete('my-project')
|
||||
* ```
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
70
packages/api-client/src/modules/labrinth/projects/v3.ts
Normal file
70
packages/api-client/src/modules/labrinth/projects/v3.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { ProjectV3 } from './types/v3'
|
||||
|
||||
export class LabrinthProjectsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_projects_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project by ID or slug (v3)
|
||||
*
|
||||
* @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI')
|
||||
* @returns Promise resolving to the v3 project data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const project = await client.labrinth.projects_v3.get('sodium')
|
||||
* console.log(project.project_types) // v3 field
|
||||
* ```
|
||||
*/
|
||||
public async get(id: string): Promise<ProjectV3> {
|
||||
return this.client.request<ProjectV3>(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple projects by IDs (v3)
|
||||
*
|
||||
* @param ids - Array of project IDs or slugs
|
||||
* @returns Promise resolving to array of v3 projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.projects_v3.getMultiple(['sodium', 'lithium'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<ProjectV3[]> {
|
||||
return this.client.request<ProjectV3[]>(`/projects`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
params: { ids: JSON.stringify(ids) },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a project (v3)
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param data - Project update data (v3 fields)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v3.edit('sodium', {
|
||||
* environment: 'client_and_server'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async edit(id: string, data: Partial<ProjectV3>): Promise<void> {
|
||||
return this.client.request(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
2
packages/api-client/src/modules/types.ts
Normal file
2
packages/api-client/src/modules/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './labrinth/projects/types/v2'
|
||||
export * from './labrinth/projects/types/v3'
|
||||
Reference in New Issue
Block a user