···4343 console.log(`🚀 API running on http://127.0.0.1:${port}`);
4444 console.log(`📚 API docs at http://127.0.0.1:${port}/api`);
4545}
4646-bootstrap();
4646+void bootstrap();
+15-9
backend/src/movies/movies.controller.ts
···1313 Logger,
1414} from '@nestjs/common';
1515import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
1616-import type { Request } from 'express';
1716import { MoviesService } from './movies.service';
1817import {
1918 SearchMoviesDto,
···2322 MarkWatchedDto,
2423} from './dto/movie.dto';
2524import { AuthGuard } from '../auth/auth.guard';
2525+import { AuthenticatedRequest } from '../auth/types';
26262727@ApiTags('movies')
2828@Controller('movies')
···5858 @ApiOperation({ summary: 'Mark a movie as watched' })
5959 @ApiResponse({ status: 201, type: TrackedMovieDto })
6060 @ApiResponse({ status: 401, description: 'Not authenticated' })
6161- async markWatched(@Body() body: MarkWatchedDto, @Req() req: Request) {
6262- const user = (req as any).user;
6161+ async markWatched(
6262+ @Body() body: MarkWatchedDto,
6363+ @Req() req: AuthenticatedRequest,
6464+ ) {
6565+ const user = req.user;
63666467 // Write to user's PDS
6568 const { uri, cid, rkey, record } = await this.moviesService.markWatched(
···8083 record.watchedAt,
8184 );
8285 return trackedMovie;
8383- } catch (err) {
8686+ } catch (err: unknown) {
8487 this.logger.warn(
8585- { err },
8888+ { err: err instanceof Error ? err.message : String(err) },
8689 'Failed to optimistically update DB; firehose will catch it',
8790 );
8891 // Return a minimal response since PDS write succeeded
···9699 @ApiOperation({ summary: 'Unmark a movie as watched' })
97100 @ApiResponse({ status: 204, description: 'Movie unmarked as watched' })
98101 @ApiResponse({ status: 401, description: 'Not authenticated' })
9999- async unmarkWatched(@Param('movieId') movieId: string, @Req() req: Request) {
100100- const user = (req as any).user;
102102+ async unmarkWatched(
103103+ @Param('movieId') movieId: string,
104104+ @Req() req: AuthenticatedRequest,
105105+ ) {
106106+ const user = req.user;
101107102108 // Delete from user's PDS
103109 await this.moviesService.unmarkWatched(user.did, user.session, movieId);
···106112 // If this fails, the firehose ingester will catch it later
107113 try {
108114 await this.moviesService.removeTrackedMovie(user.did, movieId);
109109- } catch (err) {
115115+ } catch (err: unknown) {
110116 this.logger.warn(
111111- { err },
117117+ { err: err instanceof Error ? err.message : String(err) },
112118 'Failed to optimistically remove from DB; firehose will catch it',
113119 );
114120 }
+42-15
backend/src/movies/movies.service.ts
···5566const COLLECTION = 'app.opnshelf.movie';
7788+interface TMDBMovie {
99+ id: number;
1010+ title: string;
1111+ poster_path?: string;
1212+ backdrop_path?: string;
1313+ release_date?: string;
1414+ overview?: string;
1515+}
1616+1717+interface TMDBSearchResponse {
1818+ page: number;
1919+ results: TMDBMovie[];
2020+ total_results: number;
2121+ total_pages: number;
2222+}
2323+2424+interface ATSession {
2525+ did: string;
2626+}
2727+828@Injectable()
929export class MoviesService {
1030 private readonly logger = new Logger(MoviesService.name);
···1838 this.tmdbApiKey = this.config.get('TMDB_API_KEY') ?? '';
1939 }
20402121- async searchMovies(query: string, page: number = 1) {
4141+ async searchMovies(
4242+ query: string,
4343+ page: number = 1,
4444+ ): Promise<TMDBSearchResponse> {
2245 const response = await fetch(
2346 `${this.tmdbBaseUrl}/search/movie?api_key=${this.tmdbApiKey}&query=${encodeURIComponent(query)}&page=${page}`,
2447 );
···2750 throw new Error('Failed to search movies');
2851 }
29523030- return response.json();
5353+ return response.json() as Promise<TMDBSearchResponse>;
3154 }
32553333- async getMovieDetails(movieId: string) {
5656+ async getMovieDetails(movieId: string): Promise<TMDBMovie> {
3457 const response = await fetch(
3558 `${this.tmdbBaseUrl}/movie/${movieId}?api_key=${this.tmdbApiKey}`,
3659 );
···3962 throw new Error('Movie not found');
4063 }
41644242- return response.json();
6565+ return response.json() as Promise<TMDBMovie>;
4366 }
44674568 async getUserMovies(userDid: string) {
···5679 });
5780 }
58815959- async upsertMovie(movieData: any) {
8282+ async upsertMovie(movieData: TMDBMovie) {
6083 return this.prisma.movie.upsert({
6184 where: { movieId: movieData.id.toString() },
6285 create: {
6386 movieId: movieData.id.toString(),
6487 title: movieData.title,
6565- posterPath: movieData.poster_path,
6666- backdropPath: movieData.backdrop_path,
8888+ posterPath: movieData.poster_path ?? null,
8989+ backdropPath: movieData.backdrop_path ?? null,
6790 releaseYear: movieData.release_date
6891 ? new Date(movieData.release_date).getFullYear()
6992 : null,
7093 releaseDate: movieData.release_date
7194 ? new Date(movieData.release_date)
7295 : null,
7373- overview: movieData.overview,
9696+ overview: movieData.overview ?? null,
7497 },
7598 update: {
7699 title: movieData.title,
7777- posterPath: movieData.poster_path,
7878- backdropPath: movieData.backdrop_path,
100100+ posterPath: movieData.poster_path ?? null,
101101+ backdropPath: movieData.backdrop_path ?? null,
79102 releaseYear: movieData.release_date
80103 ? new Date(movieData.release_date).getFullYear()
81104 : null,
82105 releaseDate: movieData.release_date
83106 ? new Date(movieData.release_date)
84107 : null,
8585- overview: movieData.overview,
108108+ overview: movieData.overview ?? null,
86109 },
87110 });
88111 }
···91114 * Mark a movie as watched by creating an AT Protocol record in the user's PDS.
92115 * Database indexing happens via the firehose ingester or optimistic update in controller.
93116 */
9494- async markWatched(userDid: string, session: any, movieId: string) {
117117+ async markWatched(userDid: string, session: ATSession, movieId: string) {
95118 const rkey = `movie-${movieId}`;
96119 const now = new Date().toISOString();
97120···105128 };
106129107130 // Create agent from session and write record to user's PDS
108108- const agent = new Agent(session);
131131+ const agent = new Agent(
132132+ session as unknown as ConstructorParameters<typeof Agent>[0],
133133+ );
109134 const response = await agent.com.atproto.repo.putRecord({
110135 repo: session.did,
111136 collection: COLLECTION,
···131156 * Unmark a movie as watched by deleting the AT Protocol record from the user's PDS.
132157 * Database cleanup happens via the firehose ingester or optimistic update in controller.
133158 */
134134- async unmarkWatched(userDid: string, session: any, movieId: string) {
159159+ async unmarkWatched(userDid: string, session: ATSession, movieId: string) {
135160 const rkey = `movie-${movieId}`;
136161137162 // Create agent from session and delete record from user's PDS
138138- const agent = new Agent(session);
163163+ const agent = new Agent(
164164+ session as unknown as ConstructorParameters<typeof Agent>[0],
165165+ );
139166 await agent.com.atproto.repo.deleteRecord({
140167 repo: session.did,
141168 collection: COLLECTION,
···11+// This file is auto-generated by @hey-api/openapi-ts
22+33+import { type ClientOptions, type Config, createClient, createConfig } from './client';
44+import type { ClientOptions as ClientOptions2 } from './types.gen';
55+66+/**
77+ * The `createClientConfig()` function will be called on client initialization
88+ * and the returned object will become the client's initial configuration.
99+ *
1010+ * You may want to initialize your client this way instead of calling
1111+ * `setConfig()`. This is useful for example if you're using Next.js
1212+ * to ensure your client always has the correct values.
1313+ */
1414+export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
1515+1616+export const client = createClient(createConfig<ClientOptions2>());
···11+// This file is auto-generated by @hey-api/openapi-ts
22+33+import type { Config } from './types.gen';
44+55+export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
66+ Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
77+ /**
88+ * Fetch API implementation. You can use this option to provide a custom
99+ * fetch instance.
1010+ *
1111+ * @default globalThis.fetch
1212+ */
1313+ fetch?: typeof fetch;
1414+ /**
1515+ * Implementing clients can call request interceptors inside this hook.
1616+ */
1717+ onRequest?: (url: string, init: RequestInit) => Promise<Request>;
1818+ /**
1919+ * Callback invoked when a network or parsing error occurs during streaming.
2020+ *
2121+ * This option applies only if the endpoint returns a stream of events.
2222+ *
2323+ * @param error The error that occurred.
2424+ */
2525+ onSseError?: (error: unknown) => void;
2626+ /**
2727+ * Callback invoked when an event is streamed from the server.
2828+ *
2929+ * This option applies only if the endpoint returns a stream of events.
3030+ *
3131+ * @param event Event streamed from the server.
3232+ * @returns Nothing (void).
3333+ */
3434+ onSseEvent?: (event: StreamEvent<TData>) => void;
3535+ serializedBody?: RequestInit['body'];
3636+ /**
3737+ * Default retry delay in milliseconds.
3838+ *
3939+ * This option applies only if the endpoint returns a stream of events.
4040+ *
4141+ * @default 3000
4242+ */
4343+ sseDefaultRetryDelay?: number;
4444+ /**
4545+ * Maximum number of retry attempts before giving up.
4646+ */
4747+ sseMaxRetryAttempts?: number;
4848+ /**
4949+ * Maximum retry delay in milliseconds.
5050+ *
5151+ * Applies only when exponential backoff is used.
5252+ *
5353+ * This option applies only if the endpoint returns a stream of events.
5454+ *
5555+ * @default 30000
5656+ */
5757+ sseMaxRetryDelay?: number;
5858+ /**
5959+ * Optional sleep function for retry backoff.
6060+ *
6161+ * Defaults to using `setTimeout`.
6262+ */
6363+ sseSleepFn?: (ms: number) => Promise<void>;
6464+ url: string;
6565+ };
6666+6767+export interface StreamEvent<TData = unknown> {
6868+ data: TData;
6969+ event?: string;
7070+ id?: string;
7171+ retry?: number;
7272+}
7373+7474+export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
7575+ stream: AsyncGenerator<
7676+ TData extends Record<string, unknown> ? TData[keyof TData] : TData,
7777+ TReturn,
7878+ TNext
7979+ >;
8080+};
8181+8282+export const createSseClient = <TData = unknown>({
8383+ onRequest,
8484+ onSseError,
8585+ onSseEvent,
8686+ responseTransformer,
8787+ responseValidator,
8888+ sseDefaultRetryDelay,
8989+ sseMaxRetryAttempts,
9090+ sseMaxRetryDelay,
9191+ sseSleepFn,
9292+ url,
9393+ ...options
9494+}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
9595+ let lastEventId: string | undefined;
9696+9797+ const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
9898+9999+ const createStream = async function* () {
100100+ let retryDelay: number = sseDefaultRetryDelay ?? 3000;
101101+ let attempt = 0;
102102+ const signal = options.signal ?? new AbortController().signal;
103103+104104+ while (true) {
105105+ if (signal.aborted) break;
106106+107107+ attempt++;
108108+109109+ const headers =
110110+ options.headers instanceof Headers
111111+ ? options.headers
112112+ : new Headers(options.headers as Record<string, string> | undefined);
113113+114114+ if (lastEventId !== undefined) {
115115+ headers.set('Last-Event-ID', lastEventId);
116116+ }
117117+118118+ try {
119119+ const requestInit: RequestInit = {
120120+ redirect: 'follow',
121121+ ...options,
122122+ body: options.serializedBody,
123123+ headers,
124124+ signal,
125125+ };
126126+ let request = new Request(url, requestInit);
127127+ if (onRequest) {
128128+ request = await onRequest(url, requestInit);
129129+ }
130130+ // fetch must be assigned here, otherwise it would throw the error:
131131+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
132132+ const _fetch = options.fetch ?? globalThis.fetch;
133133+ const response = await _fetch(request);
134134+135135+ if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
136136+137137+ if (!response.body) throw new Error('No body in SSE response');
138138+139139+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
140140+141141+ let buffer = '';
142142+143143+ const abortHandler = () => {
144144+ try {
145145+ reader.cancel();
146146+ } catch {
147147+ // noop
148148+ }
149149+ };
150150+151151+ signal.addEventListener('abort', abortHandler);
152152+153153+ try {
154154+ while (true) {
155155+ const { done, value } = await reader.read();
156156+ if (done) break;
157157+ buffer += value;
158158+ // Normalize line endings: CRLF -> LF, then CR -> LF
159159+ buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
160160+161161+ const chunks = buffer.split('\n\n');
162162+ buffer = chunks.pop() ?? '';
163163+164164+ for (const chunk of chunks) {
165165+ const lines = chunk.split('\n');
166166+ const dataLines: Array<string> = [];
167167+ let eventName: string | undefined;
168168+169169+ for (const line of lines) {
170170+ if (line.startsWith('data:')) {
171171+ dataLines.push(line.replace(/^data:\s*/, ''));
172172+ } else if (line.startsWith('event:')) {
173173+ eventName = line.replace(/^event:\s*/, '');
174174+ } else if (line.startsWith('id:')) {
175175+ lastEventId = line.replace(/^id:\s*/, '');
176176+ } else if (line.startsWith('retry:')) {
177177+ const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
178178+ if (!Number.isNaN(parsed)) {
179179+ retryDelay = parsed;
180180+ }
181181+ }
182182+ }
183183+184184+ let data: unknown;
185185+ let parsedJson = false;
186186+187187+ if (dataLines.length) {
188188+ const rawData = dataLines.join('\n');
189189+ try {
190190+ data = JSON.parse(rawData);
191191+ parsedJson = true;
192192+ } catch {
193193+ data = rawData;
194194+ }
195195+ }
196196+197197+ if (parsedJson) {
198198+ if (responseValidator) {
199199+ await responseValidator(data);
200200+ }
201201+202202+ if (responseTransformer) {
203203+ data = await responseTransformer(data);
204204+ }
205205+ }
206206+207207+ onSseEvent?.({
208208+ data,
209209+ event: eventName,
210210+ id: lastEventId,
211211+ retry: retryDelay,
212212+ });
213213+214214+ if (dataLines.length) {
215215+ yield data as any;
216216+ }
217217+ }
218218+ }
219219+ } finally {
220220+ signal.removeEventListener('abort', abortHandler);
221221+ reader.releaseLock();
222222+ }
223223+224224+ break; // exit loop on normal completion
225225+ } catch (error) {
226226+ // connection failed or aborted; retry after delay
227227+ onSseError?.(error);
228228+229229+ if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
230230+ break; // stop after firing error
231231+ }
232232+233233+ // exponential backoff: double retry each attempt, cap at 30s
234234+ const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
235235+ await sleep(backoff);
236236+ }
237237+ }
238238+ };
239239+240240+ const stream = createStream();
241241+242242+ return { stream };
243243+};
+104
packages/api/src/generated/core/types.gen.ts
···11+// This file is auto-generated by @hey-api/openapi-ts
22+33+import type { Auth, AuthToken } from './auth.gen';
44+import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
55+66+export type HttpMethod =
77+ | 'connect'
88+ | 'delete'
99+ | 'get'
1010+ | 'head'
1111+ | 'options'
1212+ | 'patch'
1313+ | 'post'
1414+ | 'put'
1515+ | 'trace';
1616+1717+export type Client<
1818+ RequestFn = never,
1919+ Config = unknown,
2020+ MethodFn = never,
2121+ BuildUrlFn = never,
2222+ SseFn = never,
2323+> = {
2424+ /**
2525+ * Returns the final request URL.
2626+ */
2727+ buildUrl: BuildUrlFn;
2828+ getConfig: () => Config;
2929+ request: RequestFn;
3030+ setConfig: (config: Config) => Config;
3131+} & {
3232+ [K in HttpMethod]: MethodFn;
3333+} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
3434+3535+export interface Config {
3636+ /**
3737+ * Auth token or a function returning auth token. The resolved value will be
3838+ * added to the request payload as defined by its `security` array.
3939+ */
4040+ auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
4141+ /**
4242+ * A function for serializing request body parameter. By default,
4343+ * {@link JSON.stringify()} will be used.
4444+ */
4545+ bodySerializer?: BodySerializer | null;
4646+ /**
4747+ * An object containing any HTTP headers that you want to pre-populate your
4848+ * `Headers` object with.
4949+ *
5050+ * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
5151+ */
5252+ headers?:
5353+ | RequestInit['headers']
5454+ | Record<
5555+ string,
5656+ string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
5757+ >;
5858+ /**
5959+ * The request method.
6060+ *
6161+ * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
6262+ */
6363+ method?: Uppercase<HttpMethod>;
6464+ /**
6565+ * A function for serializing request query parameters. By default, arrays
6666+ * will be exploded in form style, objects will be exploded in deepObject
6767+ * style, and reserved characters are percent-encoded.
6868+ *
6969+ * This method will have no effect if the native `paramsSerializer()` Axios
7070+ * API function is used.
7171+ *
7272+ * {@link https://swagger.io/docs/specification/serialization/#query View examples}
7373+ */
7474+ querySerializer?: QuerySerializer | QuerySerializerOptions;
7575+ /**
7676+ * A function validating request data. This is useful if you want to ensure
7777+ * the request conforms to the desired shape, so it can be safely sent to
7878+ * the server.
7979+ */
8080+ requestValidator?: (data: unknown) => Promise<unknown>;
8181+ /**
8282+ * A function transforming response data before it's returned. This is useful
8383+ * for post-processing data, e.g. converting ISO strings into Date objects.
8484+ */
8585+ responseTransformer?: (data: unknown) => Promise<unknown>;
8686+ /**
8787+ * A function validating response data. This is useful if you want to ensure
8888+ * the response conforms to the desired shape, so it can be safely passed to
8989+ * the transformers and returned to the user.
9090+ */
9191+ responseValidator?: (data: unknown) => Promise<unknown>;
9292+}
9393+9494+type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
9595+ ? true
9696+ : [T] extends [never | undefined]
9797+ ? [undefined] extends [T]
9898+ ? false
9999+ : true
100100+ : false;
101101+102102+export type OmitNever<T extends Record<string, unknown>> = {
103103+ [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
104104+};
+140
packages/api/src/generated/core/utils.gen.ts
···11+// This file is auto-generated by @hey-api/openapi-ts
22+33+import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
44+import {
55+ type ArraySeparatorStyle,
66+ serializeArrayParam,
77+ serializeObjectParam,
88+ serializePrimitiveParam,
99+} from './pathSerializer.gen';
1010+1111+export interface PathSerializer {
1212+ path: Record<string, unknown>;
1313+ url: string;
1414+}
1515+1616+export const PATH_PARAM_RE = /\{[^{}]+\}/g;
1717+1818+export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
1919+ let url = _url;
2020+ const matches = _url.match(PATH_PARAM_RE);
2121+ if (matches) {
2222+ for (const match of matches) {
2323+ let explode = false;
2424+ let name = match.substring(1, match.length - 1);
2525+ let style: ArraySeparatorStyle = 'simple';
2626+2727+ if (name.endsWith('*')) {
2828+ explode = true;
2929+ name = name.substring(0, name.length - 1);
3030+ }
3131+3232+ if (name.startsWith('.')) {
3333+ name = name.substring(1);
3434+ style = 'label';
3535+ } else if (name.startsWith(';')) {
3636+ name = name.substring(1);
3737+ style = 'matrix';
3838+ }
3939+4040+ const value = path[name];
4141+4242+ if (value === undefined || value === null) {
4343+ continue;
4444+ }
4545+4646+ if (Array.isArray(value)) {
4747+ url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
4848+ continue;
4949+ }
5050+5151+ if (typeof value === 'object') {
5252+ url = url.replace(
5353+ match,
5454+ serializeObjectParam({
5555+ explode,
5656+ name,
5757+ style,
5858+ value: value as Record<string, unknown>,
5959+ valueOnly: true,
6060+ }),
6161+ );
6262+ continue;
6363+ }
6464+6565+ if (style === 'matrix') {
6666+ url = url.replace(
6767+ match,
6868+ `;${serializePrimitiveParam({
6969+ name,
7070+ value: value as string,
7171+ })}`,
7272+ );
7373+ continue;
7474+ }
7575+7676+ const replaceValue = encodeURIComponent(
7777+ style === 'label' ? `.${value as string}` : (value as string),
7878+ );
7979+ url = url.replace(match, replaceValue);
8080+ }
8181+ }
8282+ return url;
8383+};
8484+8585+export const getUrl = ({
8686+ baseUrl,
8787+ path,
8888+ query,
8989+ querySerializer,
9090+ url: _url,
9191+}: {
9292+ baseUrl?: string;
9393+ path?: Record<string, unknown>;
9494+ query?: Record<string, unknown>;
9595+ querySerializer: QuerySerializer;
9696+ url: string;
9797+}) => {
9898+ const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
9999+ let url = (baseUrl ?? '') + pathUrl;
100100+ if (path) {
101101+ url = defaultPathSerializer({ path, url });
102102+ }
103103+ let search = query ? querySerializer(query) : '';
104104+ if (search.startsWith('?')) {
105105+ search = search.substring(1);
106106+ }
107107+ if (search) {
108108+ url += `?${search}`;
109109+ }
110110+ return url;
111111+};
112112+113113+export function getValidRequestBody(options: {
114114+ body?: unknown;
115115+ bodySerializer?: BodySerializer | null;
116116+ serializedBody?: unknown;
117117+}) {
118118+ const hasBody = options.body !== undefined;
119119+ const isSerializedBody = hasBody && options.bodySerializer;
120120+121121+ if (isSerializedBody) {
122122+ if ('serializedBody' in options) {
123123+ const hasSerializedBody =
124124+ options.serializedBody !== undefined && options.serializedBody !== '';
125125+126126+ return hasSerializedBody ? options.serializedBody : null;
127127+ }
128128+129129+ // not all clients implement a serializedBody property (i.e. client-axios)
130130+ return options.body !== '' ? options.body : null;
131131+ }
132132+133133+ // plain/text body
134134+ if (hasBody) {
135135+ return options.body;
136136+ }
137137+138138+ // no body was provided
139139+ return undefined;
140140+}
+4
packages/api/src/generated/index.ts
···11+// This file is auto-generated by @hey-api/openapi-ts
22+33+export { appControllerGetHello, authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetUserMovies, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options } from './sdk.gen';
44+export type { AppControllerGetHelloData, AppControllerGetHelloResponses, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponse, AuthControllerMeResponses, ClientOptions, MarkWatchedDto, MovieDto, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponse, MoviesControllerGetMovieResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesResponse, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponse, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponse, MoviesControllerUnmarkWatchedResponses, SearchResultsDto, TmdbMovieResultDto, TrackedMovieDto, UserDto } from './types.gen';