ani-client 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -101,6 +101,28 @@ interface Studio {
101
101
  interface StudioConnection {
102
102
  nodes: Studio[];
103
103
  }
104
+ declare enum MediaRelationType {
105
+ ADAPTATION = "ADAPTATION",
106
+ PREQUEL = "PREQUEL",
107
+ SEQUEL = "SEQUEL",
108
+ PARENT = "PARENT",
109
+ SIDE_STORY = "SIDE_STORY",
110
+ CHARACTER = "CHARACTER",
111
+ SUMMARY = "SUMMARY",
112
+ ALTERNATIVE = "ALTERNATIVE",
113
+ SPIN_OFF = "SPIN_OFF",
114
+ OTHER = "OTHER",
115
+ SOURCE = "SOURCE",
116
+ COMPILATION = "COMPILATION",
117
+ CONTAINS = "CONTAINS"
118
+ }
119
+ interface MediaEdge {
120
+ relationType: MediaRelationType;
121
+ node: Pick<Media, "id" | "title" | "type" | "format" | "status" | "coverImage" | "siteUrl">;
122
+ }
123
+ interface MediaConnection {
124
+ edges: MediaEdge[];
125
+ }
104
126
  interface CharacterName {
105
127
  first: string | null;
106
128
  middle: string | null;
@@ -145,6 +167,7 @@ interface Media {
145
167
  trending: number | null;
146
168
  tags: MediaTag[];
147
169
  studios: StudioConnection;
170
+ relations: MediaConnection | null;
148
171
  isAdult: boolean | null;
149
172
  siteUrl: string | null;
150
173
  }
@@ -288,6 +311,163 @@ interface GetPlanningOptions {
288
311
  page?: number;
289
312
  perPage?: number;
290
313
  }
314
+ declare enum RecommendationSort {
315
+ ID = "ID",
316
+ ID_DESC = "ID_DESC",
317
+ RATING = "RATING",
318
+ RATING_DESC = "RATING_DESC"
319
+ }
320
+ interface Recommendation {
321
+ id: number;
322
+ rating: number | null;
323
+ userRating: string | null;
324
+ mediaRecommendation: Media;
325
+ user: {
326
+ id: number;
327
+ name: string;
328
+ avatar: UserAvatar;
329
+ } | null;
330
+ }
331
+ interface GetRecommendationsOptions {
332
+ /** The AniList media ID to get recommendations for */
333
+ mediaId: number;
334
+ /** Sort order (default: RATING_DESC) */
335
+ sort?: RecommendationSort[];
336
+ page?: number;
337
+ perPage?: number;
338
+ }
339
+ declare enum MediaListStatus {
340
+ CURRENT = "CURRENT",
341
+ PLANNING = "PLANNING",
342
+ COMPLETED = "COMPLETED",
343
+ DROPPED = "DROPPED",
344
+ PAUSED = "PAUSED",
345
+ REPEATING = "REPEATING"
346
+ }
347
+ declare enum MediaListSort {
348
+ MEDIA_ID = "MEDIA_ID",
349
+ MEDIA_ID_DESC = "MEDIA_ID_DESC",
350
+ SCORE = "SCORE",
351
+ SCORE_DESC = "SCORE_DESC",
352
+ STATUS = "STATUS",
353
+ STATUS_DESC = "STATUS_DESC",
354
+ PROGRESS = "PROGRESS",
355
+ PROGRESS_DESC = "PROGRESS_DESC",
356
+ PROGRESS_VOLUMES = "PROGRESS_VOLUMES",
357
+ PROGRESS_VOLUMES_DESC = "PROGRESS_VOLUMES_DESC",
358
+ REPEAT = "REPEAT",
359
+ REPEAT_DESC = "REPEAT_DESC",
360
+ PRIORITY = "PRIORITY",
361
+ PRIORITY_DESC = "PRIORITY_DESC",
362
+ STARTED_ON = "STARTED_ON",
363
+ STARTED_ON_DESC = "STARTED_ON_DESC",
364
+ FINISHED_ON = "FINISHED_ON",
365
+ FINISHED_ON_DESC = "FINISHED_ON_DESC",
366
+ ADDED_TIME = "ADDED_TIME",
367
+ ADDED_TIME_DESC = "ADDED_TIME_DESC",
368
+ UPDATED_TIME = "UPDATED_TIME",
369
+ UPDATED_TIME_DESC = "UPDATED_TIME_DESC",
370
+ MEDIA_TITLE_ROMAJI = "MEDIA_TITLE_ROMAJI",
371
+ MEDIA_TITLE_ROMAJI_DESC = "MEDIA_TITLE_ROMAJI_DESC",
372
+ MEDIA_TITLE_ENGLISH = "MEDIA_TITLE_ENGLISH",
373
+ MEDIA_TITLE_ENGLISH_DESC = "MEDIA_TITLE_ENGLISH_DESC",
374
+ MEDIA_TITLE_NATIVE = "MEDIA_TITLE_NATIVE",
375
+ MEDIA_TITLE_NATIVE_DESC = "MEDIA_TITLE_NATIVE_DESC",
376
+ MEDIA_POPULARITY = "MEDIA_POPULARITY",
377
+ MEDIA_POPULARITY_DESC = "MEDIA_POPULARITY_DESC"
378
+ }
379
+ interface MediaListEntry {
380
+ id: number;
381
+ mediaId: number;
382
+ status: MediaListStatus;
383
+ score: number | null;
384
+ progress: number | null;
385
+ progressVolumes: number | null;
386
+ repeat: number | null;
387
+ priority: number | null;
388
+ private: boolean | null;
389
+ notes: string | null;
390
+ startedAt: FuzzyDate | null;
391
+ completedAt: FuzzyDate | null;
392
+ updatedAt: number | null;
393
+ createdAt: number | null;
394
+ media: Media;
395
+ }
396
+ interface GetSeasonOptions {
397
+ /** The season (WINTER, SPRING, SUMMER, FALL) */
398
+ season: MediaSeason;
399
+ /** The year */
400
+ seasonYear: number;
401
+ /** Filter by ANIME or MANGA (defaults to ANIME) */
402
+ type?: MediaType;
403
+ /** Sort order (default: POPULARITY_DESC) */
404
+ sort?: MediaSort[];
405
+ page?: number;
406
+ perPage?: number;
407
+ }
408
+ interface GetUserMediaListOptions {
409
+ /** User ID (provide either userId or userName) */
410
+ userId?: number;
411
+ /** Username (provide either userId or userName) */
412
+ userName?: string;
413
+ /** ANIME or MANGA */
414
+ type: MediaType;
415
+ /** Filter by list status (CURRENT, COMPLETED, etc.) */
416
+ status?: MediaListStatus;
417
+ /** Sort order */
418
+ sort?: MediaListSort[];
419
+ page?: number;
420
+ perPage?: number;
421
+ }
422
+ interface StudioDetail {
423
+ id: number;
424
+ name: string;
425
+ isAnimationStudio: boolean;
426
+ siteUrl: string | null;
427
+ favourites: number | null;
428
+ media: {
429
+ pageInfo: PageInfo;
430
+ nodes: Pick<Media, "id" | "title" | "type" | "format" | "coverImage" | "siteUrl">[];
431
+ } | null;
432
+ }
433
+ interface SearchStudioOptions {
434
+ query?: string;
435
+ page?: number;
436
+ perPage?: number;
437
+ }
438
+ /**
439
+ * Interface that all cache adapters must implement.
440
+ * Methods may return sync values or Promises — the client awaits all calls.
441
+ */
442
+ interface CacheAdapter {
443
+ /** Retrieve a cached value, or `undefined` if missing / expired. */
444
+ get<T>(key: string): T | undefined | Promise<T | undefined>;
445
+ /** Store a value in the cache. */
446
+ set<T>(key: string, data: T): void | Promise<void>;
447
+ /** Remove a specific entry. Returns `true` if the key existed. */
448
+ delete(key: string): boolean | Promise<boolean>;
449
+ /** Clear the entire cache. */
450
+ clear(): void | Promise<void>;
451
+ /** Number of entries currently stored (sync). Returns -1 if unknown. */
452
+ readonly size: number;
453
+ /** Return all cache keys. */
454
+ keys(): IterableIterator<string> | string[] | Promise<string[]>;
455
+ /** Bulk-remove entries matching a pattern. Optional — the client provides a fallback. */
456
+ invalidate?(pattern: string | RegExp): number | Promise<number>;
457
+ }
458
+ /** Event hooks for logging, debugging, and monitoring. */
459
+ interface AniListHooks {
460
+ /** Called before every API request. */
461
+ onRequest?: (query: string, variables: Record<string, unknown>) => void;
462
+ /** Called when a response is served from cache. */
463
+ onCacheHit?: (key: string) => void;
464
+ /** Called when the rate limiter enforces a wait (429 received). */
465
+ onRateLimit?: (retryAfterMs: number) => void;
466
+ /** Called when a request is retried (429 or network error). */
467
+ onRetry?: (attempt: number, reason: string, delayMs: number) => void;
468
+ /** Called when a request completes. */
469
+ onResponse?: (query: string, durationMs: number, fromCache: boolean) => void;
470
+ }
291
471
  interface AniListClientOptions {
292
472
  /** Optional AniList OAuth token for authenticated requests */
293
473
  token?: string;
@@ -302,6 +482,8 @@ interface AniListClientOptions {
302
482
  /** Set to false to disable caching entirely */
303
483
  enabled?: boolean;
304
484
  };
485
+ /** Custom cache adapter (e.g. RedisCache). Takes precedence over `cache`. */
486
+ cacheAdapter?: CacheAdapter;
305
487
  /** Rate limiter configuration (enabled by default, 85 req/min) */
306
488
  rateLimit?: {
307
489
  /** Max requests per window (default: 85) */
@@ -314,7 +496,13 @@ interface AniListClientOptions {
314
496
  retryDelayMs?: number;
315
497
  /** Set to false to disable rate limiting entirely */
316
498
  enabled?: boolean;
499
+ /** Timeout per request in ms (default: 30 000). 0 = no timeout. */
500
+ timeoutMs?: number;
501
+ /** Retry on network errors like ECONNRESET / ETIMEDOUT (default: true) */
502
+ retryOnNetworkError?: boolean;
317
503
  };
504
+ /** Event hooks for logging, debugging, and monitoring */
505
+ hooks?: AniListHooks;
318
506
  }
319
507
 
320
508
  /**
@@ -338,13 +526,22 @@ interface AniListClientOptions {
338
526
  declare class AniListClient {
339
527
  private readonly apiUrl;
340
528
  private readonly headers;
341
- private readonly cache;
529
+ private readonly cacheAdapter;
342
530
  private readonly rateLimiter;
531
+ private readonly hooks;
532
+ private readonly inFlight;
343
533
  constructor(options?: AniListClientOptions);
344
534
  /**
345
535
  * @internal
346
536
  */
347
537
  private request;
538
+ /** @internal */
539
+ private executeRequest;
540
+ /**
541
+ * @internal
542
+ * Shorthand for paginated queries that follow the `Page { pageInfo, <field>[] }` pattern.
543
+ */
544
+ private pagedRequest;
348
545
  /**
349
546
  * Fetch a single media entry by its AniList ID.
350
547
  *
@@ -455,14 +652,171 @@ declare class AniListClient {
455
652
  * ```
456
653
  */
457
654
  getPlanning(options?: GetPlanningOptions): Promise<PagedResult<Media>>;
655
+ /**
656
+ * Get recommendations for a specific media.
657
+ *
658
+ * Returns other anime/manga that users have recommended based on the given media.
659
+ *
660
+ * @param mediaId - The AniList media ID
661
+ * @param options - Optional sort / pagination parameters
662
+ * @returns Paginated list of recommendations
663
+ *
664
+ * @example
665
+ * ```ts
666
+ * // Get recommendations for Cowboy Bebop
667
+ * const recs = await client.getRecommendations(1);
668
+ * recs.results.forEach((r) =>
669
+ * console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
670
+ * );
671
+ * ```
672
+ */
673
+ getRecommendations(mediaId: number, options?: Omit<GetRecommendationsOptions, "mediaId">): Promise<PagedResult<Recommendation>>;
674
+ /**
675
+ * Get anime (or manga) for a specific season and year.
676
+ *
677
+ * @param options - Season, year and optional filter / pagination parameters
678
+ * @returns Paginated list of media for the given season
679
+ *
680
+ * @example
681
+ * ```ts
682
+ * import { MediaSeason } from "ani-client";
683
+ *
684
+ * const winter2026 = await client.getMediaBySeason({
685
+ * season: MediaSeason.WINTER,
686
+ * seasonYear: 2026,
687
+ * perPage: 10,
688
+ * });
689
+ * ```
690
+ */
691
+ getMediaBySeason(options: GetSeasonOptions): Promise<PagedResult<Media>>;
692
+ /**
693
+ * Get a user's anime or manga list.
694
+ *
695
+ * Provide either `userId` or `userName` to identify the user.
696
+ * Requires `type` (ANIME or MANGA). Optionally filter by list status.
697
+ *
698
+ * @param options - User identifier, media type, and optional filters
699
+ * @returns Paginated list of media list entries
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * import { MediaType, MediaListStatus } from "ani-client";
704
+ *
705
+ * // Get a user's completed anime list
706
+ * const list = await client.getUserMediaList({
707
+ * userName: "AniList",
708
+ * type: MediaType.ANIME,
709
+ * status: MediaListStatus.COMPLETED,
710
+ * });
711
+ * list.results.forEach((entry) =>
712
+ * console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
713
+ * );
714
+ * ```
715
+ */
716
+ getUserMediaList(options: GetUserMediaListOptions): Promise<PagedResult<MediaListEntry>>;
717
+ /**
718
+ * Fetch a studio by its AniList ID.
719
+ *
720
+ * Returns studio details along with its most popular productions.
721
+ *
722
+ * @param id - The AniList studio ID
723
+ */
724
+ getStudio(id: number): Promise<StudioDetail>;
725
+ /**
726
+ * Search for studios by name.
727
+ *
728
+ * @param options - Search / pagination parameters
729
+ * @returns Paginated list of studios
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * const studios = await client.searchStudios({ query: "MAPPA" });
734
+ * ```
735
+ */
736
+ searchStudios(options?: SearchStudioOptions): Promise<PagedResult<StudioDetail>>;
737
+ /**
738
+ * Get all available genres on AniList.
739
+ *
740
+ * @returns Array of genre strings (e.g. "Action", "Adventure", ...)
741
+ */
742
+ getGenres(): Promise<string[]>;
743
+ /**
744
+ * Get all available media tags on AniList.
745
+ *
746
+ * @returns Array of tag objects with id, name, description, category, isAdult
747
+ */
748
+ getTags(): Promise<MediaTag[]>;
749
+ /**
750
+ * Auto-paginating async iterator.
751
+ *
752
+ * Wraps any paginated method and yields individual items across all pages.
753
+ * Stops when `hasNextPage` is `false` or `maxPages` is reached.
754
+ *
755
+ * @param fetchPage - A function that takes a page number and returns a `PagedResult<T>`
756
+ * @param maxPages - Maximum number of pages to fetch (default: Infinity)
757
+ * @returns An async iterable iterator of individual items
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * // Iterate over all search results
762
+ * for await (const anime of client.paginate((page) =>
763
+ * client.searchMedia({ query: "Naruto", page, perPage: 10 })
764
+ * )) {
765
+ * console.log(anime.title.romaji);
766
+ * }
767
+ *
768
+ * // Limit to 3 pages
769
+ * for await (const anime of client.paginate(
770
+ * (page) => client.getTrending(MediaType.ANIME, page, 20),
771
+ * 3,
772
+ * )) {
773
+ * console.log(anime.title.romaji);
774
+ * }
775
+ * ```
776
+ */
777
+ paginate<T>(fetchPage: (page: number) => Promise<PagedResult<T>>, maxPages?: number): AsyncGenerator<T, void, undefined>;
778
+ /**
779
+ * Fetch multiple media entries in a single API request.
780
+ * Uses GraphQL aliases to batch up to 50 IDs per call.
781
+ *
782
+ * @param ids - Array of AniList media IDs
783
+ * @returns Array of media objects (same order as input IDs)
784
+ */
785
+ getMediaBatch(ids: number[]): Promise<Media[]>;
786
+ /**
787
+ * Fetch multiple characters in a single API request.
788
+ *
789
+ * @param ids - Array of AniList character IDs
790
+ * @returns Array of character objects (same order as input IDs)
791
+ */
792
+ getCharacterBatch(ids: number[]): Promise<Character[]>;
793
+ /**
794
+ * Fetch multiple staff members in a single API request.
795
+ *
796
+ * @param ids - Array of AniList staff IDs
797
+ * @returns Array of staff objects (same order as input IDs)
798
+ */
799
+ getStaffBatch(ids: number[]): Promise<Staff[]>;
800
+ /** @internal */
801
+ private executeBatch;
802
+ /** @internal */
803
+ private chunk;
458
804
  /**
459
805
  * Clear the entire response cache.
460
806
  */
461
- clearCache(): void;
807
+ clearCache(): Promise<void>;
462
808
  /**
463
- * Number of entries currently in the cache.
809
+ * Number of entries currently in the cache (sync).
810
+ * For async adapters like Redis, this may be approximate.
464
811
  */
465
812
  get cacheSize(): number;
813
+ /**
814
+ * Remove cache entries whose key matches the given pattern.
815
+ *
816
+ * @param pattern — A string (converted to RegExp) or RegExp
817
+ * @returns Number of entries removed
818
+ */
819
+ invalidateCache(pattern: string | RegExp): Promise<number>;
466
820
  }
467
821
 
468
822
  /**
@@ -488,7 +842,7 @@ interface CacheOptions {
488
842
  /** Disable caching entirely (default: false) */
489
843
  enabled?: boolean;
490
844
  }
491
- declare class MemoryCache {
845
+ declare class MemoryCache implements CacheAdapter {
492
846
  private readonly ttl;
493
847
  private readonly maxSize;
494
848
  private readonly enabled;
@@ -506,6 +860,74 @@ declare class MemoryCache {
506
860
  clear(): void;
507
861
  /** Number of entries currently stored. */
508
862
  get size(): number;
863
+ /** Return an iterator over all cache keys. */
864
+ keys(): IterableIterator<string>;
865
+ /**
866
+ * Remove all entries whose key matches the given pattern.
867
+ *
868
+ * @param pattern — A string (converted to RegExp) or RegExp.
869
+ * @returns Number of entries removed.
870
+ */
871
+ invalidate(pattern: string | RegExp): number;
872
+ }
873
+
874
+ /**
875
+ * Minimal interface representing a Redis client.
876
+ * Compatible with both `ioredis` and `redis` (node-redis v4+).
877
+ */
878
+ interface RedisLikeClient {
879
+ get(key: string): Promise<string | null>;
880
+ set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
881
+ del(...keys: (string | string[])[]): Promise<number>;
882
+ keys(pattern: string): Promise<string[]>;
883
+ }
884
+ interface RedisCacheOptions {
885
+ /** A Redis client instance (ioredis or node-redis). */
886
+ client: RedisLikeClient;
887
+ /** Key prefix to namespace ani-client entries (default: `"ani:"`) */
888
+ prefix?: string;
889
+ /** TTL in seconds (default: 86 400 = 24 h) */
890
+ ttl?: number;
891
+ }
892
+ /**
893
+ * Redis-backed cache adapter for AniListClient.
894
+ *
895
+ * @example
896
+ * ```ts
897
+ * import Redis from "ioredis";
898
+ * import { AniListClient, RedisCache } from "ani-client";
899
+ *
900
+ * const redis = new Redis();
901
+ * const client = new AniListClient({
902
+ * cacheAdapter: new RedisCache({ client: redis }),
903
+ * });
904
+ * ```
905
+ */
906
+ declare class RedisCache implements CacheAdapter {
907
+ private readonly client;
908
+ private readonly prefix;
909
+ private readonly ttl;
910
+ constructor(options: RedisCacheOptions);
911
+ private prefixedKey;
912
+ get<T>(key: string): Promise<T | undefined>;
913
+ set<T>(key: string, data: T): Promise<void>;
914
+ delete(key: string): Promise<boolean>;
915
+ clear(): Promise<void>;
916
+ /**
917
+ * Returns -1 because Redis keys can expire silently via TTL.
918
+ * Use `getSize()` for an accurate count.
919
+ */
920
+ get size(): number;
921
+ /** Get the actual number of keys with this prefix in Redis. */
922
+ getSize(): Promise<number>;
923
+ keys(): Promise<string[]>;
924
+ /**
925
+ * Remove all entries whose key matches the given glob pattern.
926
+ *
927
+ * @param pattern — A glob pattern (e.g. `"*Media*"`)
928
+ * @returns Number of entries removed.
929
+ */
930
+ invalidate(pattern: string): Promise<number>;
509
931
  }
510
932
 
511
933
  /**
@@ -526,6 +948,10 @@ interface RateLimitOptions {
526
948
  retryDelayMs?: number;
527
949
  /** Disable rate limiting entirely (default: false) */
528
950
  enabled?: boolean;
951
+ /** Timeout per request in milliseconds (default: 30 000). 0 = no timeout. */
952
+ timeoutMs?: number;
953
+ /** Retry on network errors like ECONNRESET / ETIMEDOUT (default: true) */
954
+ retryOnNetworkError?: boolean;
529
955
  }
530
956
  declare class RateLimiter {
531
957
  private readonly maxRequests;
@@ -533,6 +959,8 @@ declare class RateLimiter {
533
959
  private readonly maxRetries;
534
960
  private readonly retryDelayMs;
535
961
  private readonly enabled;
962
+ private readonly timeoutMs;
963
+ private readonly retryOnNetworkError;
536
964
  /** @internal */
537
965
  private timestamps;
538
966
  constructor(options?: RateLimitOptions);
@@ -541,10 +969,15 @@ declare class RateLimiter {
541
969
  */
542
970
  acquire(): Promise<void>;
543
971
  /**
544
- * Execute a fetch with automatic retry on 429 responses.
972
+ * Execute a fetch with automatic retry on 429 responses and network errors.
545
973
  */
546
- fetchWithRetry(url: string, init: RequestInit): Promise<Response>;
974
+ fetchWithRetry(url: string, init: RequestInit, hooks?: {
975
+ onRetry?: (attempt: number, reason: string, delayMs: number) => void;
976
+ onRateLimit?: (retryAfterMs: number) => void;
977
+ }): Promise<Response>;
978
+ /** @internal */
979
+ private fetchWithTimeout;
547
980
  private sleep;
548
981
  }
549
982
 
550
- export { type AiringSchedule, AiringSort, AniListClient, type AniListClientOptions, AniListError, type CacheOptions, type Character, type CharacterImage, type CharacterName, CharacterSort, type FuzzyDate, type GetAiringOptions, type GetPlanningOptions, type GetRecentChaptersOptions, type Media, type MediaCoverImage, MediaFormat, MediaSeason, MediaSort, MediaStatus, type MediaTag, type MediaTitle, type MediaTrailer, MediaType, MemoryCache, type PageInfo, type PagedResult, type RateLimitOptions, RateLimiter, type SearchCharacterOptions, type SearchMediaOptions, type SearchStaffOptions, type Staff, type StaffImage, type StaffName, type Studio, type StudioConnection, type User, type UserAvatar, type UserStatistics };
983
+ export { type AiringSchedule, AiringSort, AniListClient, type AniListClientOptions, AniListError, type AniListHooks, type CacheAdapter, type CacheOptions, type Character, type CharacterImage, type CharacterName, CharacterSort, type FuzzyDate, type GetAiringOptions, type GetPlanningOptions, type GetRecentChaptersOptions, type GetRecommendationsOptions, type GetSeasonOptions, type GetUserMediaListOptions, type Media, type MediaConnection, type MediaCoverImage, type MediaEdge, MediaFormat, type MediaListEntry, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaStatus, type MediaTag, type MediaTitle, type MediaTrailer, MediaType, MemoryCache, type PageInfo, type PagedResult, type RateLimitOptions, RateLimiter, type Recommendation, RecommendationSort, RedisCache, type RedisCacheOptions, type RedisLikeClient, type SearchCharacterOptions, type SearchMediaOptions, type SearchStaffOptions, type SearchStudioOptions, type Staff, type StaffImage, type StaffName, type Studio, type StudioConnection, type StudioDetail, type User, type UserAvatar, type UserStatistics };