mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
fix: remove @sofa/core and @sofa/api deps from public-api to fix Vercel runtime compatibility
- Drop `@sofa/core` and `@sofa/api` from `apps/public-api` — importing workspace packages with DB/Node dependencies broke the Vercel edge runtime - `fetchUserData` on Trakt and Simkl importers now returns raw `unknown` API responses; parsing is deferred to the self-hosted server - Add `source` + `rawPayload` fields to `ParsePayloadInput`; `parsePayload` procedure now dispatches to `parseTraktPayload`/`parseSimklPayload` and returns full warnings + diagnostics - Export `ProviderEnum` from `apps/public-api/src/importers/index.ts` instead of defining it inline in `app.ts`
This commit is contained in:
@@ -36,10 +36,10 @@
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react-native": "3.40.0",
|
||||
"@tanstack/query-async-storage-persister": "5.95.0",
|
||||
"@tanstack/query-async-storage-persister": "5.95.2",
|
||||
"@tanstack/react-form": "1.28.5",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-query-persist-client": "5.95.0",
|
||||
"@tanstack/react-query-persist-client": "5.95.2",
|
||||
"better-auth": "catalog:",
|
||||
"burnt": "0.13.0",
|
||||
"expo": "55.0.8",
|
||||
@@ -66,7 +66,7 @@
|
||||
"expo-updates": "55.0.15",
|
||||
"expo-web-browser": "55.0.10",
|
||||
"expo-widgets": "55.0.7",
|
||||
"posthog-react-native": "4.37.5",
|
||||
"posthog-react-native": "4.37.6",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-native": "0.83.2",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=3002 bun --watch src/app.ts",
|
||||
"start": "PORT=3002 bun src/app.ts",
|
||||
"dev": "PORT=3002 bun --hot src/app.ts",
|
||||
"start": "bun src/app.ts",
|
||||
"lint": "oxlint",
|
||||
"format": "oxfmt --config ../../.oxfmtrc.json",
|
||||
"format:check": "oxfmt --check --config ../../.oxfmtrc.json",
|
||||
@@ -13,10 +13,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/zod-validator": "0.7.6",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/core": "workspace:*",
|
||||
"@vercel/firewall": "1.1.2",
|
||||
"hono": "4.12.8",
|
||||
"hono": "4.12.9",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,15 +2,17 @@ import { zValidator } from "@hono/zod-validator";
|
||||
import { checkRateLimit } from "@vercel/firewall";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { logger } from "hono/logger";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getImporter, getImporterConfig } from "./importers";
|
||||
import { getImporter, getImporterConfig, ProviderEnum } from "./importers";
|
||||
|
||||
const GITHUB_RELEASES_URL = "https://api.github.com/repos/jakejarvis/sofa/releases/latest";
|
||||
const VERSION_PREFIX_RE = /^v/;
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.use("*", logger());
|
||||
app.use("*", cors());
|
||||
|
||||
// ─── Version Check ──────────────────────────────────────────
|
||||
@@ -101,8 +103,6 @@ app.post(
|
||||
// The self-hosted server also prevents concurrent imports per user
|
||||
// via the importJobs table.
|
||||
|
||||
const ProviderEnum = z.enum(["trakt", "simkl"]);
|
||||
|
||||
app.post(
|
||||
"/v1/import/:provider/device-code",
|
||||
zValidator(
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import * as z from "zod";
|
||||
|
||||
import { simkl } from "./simkl";
|
||||
import { trakt } from "./trakt";
|
||||
import type { ImportProvider } from "./types";
|
||||
|
||||
const importers: Record<string, ImportProvider> = {
|
||||
export const ProviderEnum = z.enum(["trakt", "simkl"]);
|
||||
|
||||
const importers: Record<z.infer<typeof ProviderEnum>, ImportProvider> = {
|
||||
trakt,
|
||||
simkl,
|
||||
};
|
||||
|
||||
export function getImporter(name: string): ImportProvider | undefined {
|
||||
export function getImporter(name: z.infer<typeof ProviderEnum>): ImportProvider | undefined {
|
||||
return importers[name];
|
||||
}
|
||||
|
||||
export function getImporterConfig(name: string): {
|
||||
export function getImporterConfig(name: z.infer<typeof ProviderEnum>): {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
} {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { parseSimklPayload } from "@sofa/core/imports/parsers";
|
||||
|
||||
import type { DeviceCodeResponse, ImportProvider, NormalizedImport, PollResult } from "./types";
|
||||
import type { DeviceCodeResponse, ImportProvider, PollResult } from "./types";
|
||||
|
||||
const API_BASE = "https://api.simkl.com";
|
||||
const AUTH_BASE = "https://simkl.com";
|
||||
@@ -108,7 +106,7 @@ export const simkl: ImportProvider = {
|
||||
return { status: "pending" };
|
||||
},
|
||||
|
||||
async fetchUserData(accessToken, clientId): Promise<NormalizedImport> {
|
||||
async fetchUserData(accessToken, clientId): Promise<unknown> {
|
||||
const headers = simklHeaders(clientId, accessToken);
|
||||
|
||||
// Fetch movies, shows, and anime in parallel
|
||||
@@ -131,14 +129,11 @@ export const simkl: ImportProvider = {
|
||||
animeRes.ok ? (animeRes.json() as Promise<SimklApiItem[]>) : ([] as SimklApiItem[]),
|
||||
]);
|
||||
|
||||
// Flatten API's nested movie/show objects into the flat format
|
||||
// the core parser expects, then delegate normalization
|
||||
const result = parseSimklPayload({
|
||||
// Return flattened API response — parsing happens on the self-hosted server
|
||||
return {
|
||||
movies: flattenSimklItems(moviesData, "movie"),
|
||||
shows: flattenSimklItems(showsData, "show"),
|
||||
anime: flattenSimklItems(animeData, "show"),
|
||||
});
|
||||
|
||||
return result.data;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { parseTraktPayload } from "@sofa/core/imports/parsers";
|
||||
|
||||
import type { DeviceCodeResponse, ImportProvider, NormalizedImport, PollResult } from "./types";
|
||||
import type { DeviceCodeResponse, ImportProvider, PollResult } from "./types";
|
||||
|
||||
const API_BASE = "https://api.trakt.tv";
|
||||
|
||||
@@ -55,7 +53,7 @@ export const trakt: ImportProvider = {
|
||||
return { status: "expired" };
|
||||
},
|
||||
|
||||
async fetchUserData(accessToken, clientId): Promise<NormalizedImport> {
|
||||
async fetchUserData(accessToken, clientId): Promise<unknown> {
|
||||
const headers = traktHeaders(clientId, accessToken);
|
||||
|
||||
// Fetch all data in parallel
|
||||
@@ -82,15 +80,11 @@ export const trakt: ImportProvider = {
|
||||
ratingsRes.ok ? ratingsRes.json() : [],
|
||||
]);
|
||||
|
||||
// Restructure API response into the format parseTraktPayload expects.
|
||||
// The Trakt API returns the same item shapes as the JSON export format.
|
||||
type TraktPayload = Parameters<typeof parseTraktPayload>[0];
|
||||
const result = parseTraktPayload({
|
||||
// Return raw aggregated API response — parsing happens on the self-hosted server
|
||||
return {
|
||||
history: { movies: moviesData, shows: showsData },
|
||||
watchlist: watchlistData,
|
||||
ratings: ratingsData,
|
||||
} as TraktPayload);
|
||||
|
||||
return result.data;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { NormalizedImport } from "@sofa/api/schemas";
|
||||
|
||||
export type { NormalizedImport };
|
||||
|
||||
export interface DeviceCodeResponse {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
@@ -19,5 +15,5 @@ export type PollResult =
|
||||
export interface ImportProvider {
|
||||
getDeviceCode(clientId: string, clientSecret: string): Promise<DeviceCodeResponse>;
|
||||
pollForToken(clientId: string, clientSecret: string, deviceCode: string): Promise<PollResult>;
|
||||
fetchUserData(accessToken: string, clientId: string): Promise<NormalizedImport>;
|
||||
fetchUserData(accessToken: string, clientId: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@sofa/logger": "workspace:*",
|
||||
"@sofa/tmdb": "workspace:*",
|
||||
"croner": "10.0.2-dev.2",
|
||||
"hono": "4.12.8",
|
||||
"hono": "4.12.9",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ORPCError } from "@orpc/server";
|
||||
import { AppErrorCode } from "@sofa/api/errors";
|
||||
import type { ParseResult } from "@sofa/core/imports";
|
||||
import {
|
||||
countUnresolved,
|
||||
getActiveImportJobForUser,
|
||||
insertImportJob,
|
||||
parseLetterboxdExport,
|
||||
@@ -87,17 +86,27 @@ export const parseFile = os.imports.parseFile.use(authed).handler(async ({ input
|
||||
});
|
||||
|
||||
export const parsePayload = os.imports.parsePayload.use(authed).handler(({ input }) => {
|
||||
const { data } = input;
|
||||
const { source, rawPayload } = input;
|
||||
|
||||
let result: ParseResult;
|
||||
switch (source) {
|
||||
case "trakt":
|
||||
result = parseTraktPayload(rawPayload as Parameters<typeof parseTraktPayload>[0]);
|
||||
break;
|
||||
case "simkl":
|
||||
result = parseSimklPayload(rawPayload as Parameters<typeof parseSimklPayload>[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
warnings: [],
|
||||
diagnostics: { unresolved: countUnresolved(data), unsupported: 0 },
|
||||
data: result.data,
|
||||
warnings: result.warnings,
|
||||
diagnostics: result.diagnostics,
|
||||
stats: {
|
||||
movies: data.movies.length,
|
||||
episodes: data.episodes.length,
|
||||
watchlist: data.watchlist.length,
|
||||
ratings: data.ratings.length,
|
||||
movies: result.data.movies.length,
|
||||
episodes: result.data.episodes.length,
|
||||
watchlist: result.data.watchlist.length,
|
||||
ratings: result.data.ratings.length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-hotkeys": "0.5.1",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.168.2",
|
||||
"@tanstack/react-router": "1.168.3",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -53,9 +53,9 @@
|
||||
"@tailwindcss/vite": "4.2.2",
|
||||
"@tanstack/devtools-vite": "0.6.0",
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.95.0",
|
||||
"@tanstack/react-query-devtools": "5.95.2",
|
||||
"@tanstack/react-router-devtools": "1.166.11",
|
||||
"@tanstack/router-plugin": "1.167.3",
|
||||
"@tanstack/router-plugin": "1.167.4",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
@@ -66,7 +66,7 @@
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.0.1",
|
||||
"vite": "8.0.2",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +416,10 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
if (data.status === "authorized" && data.data) {
|
||||
stopPolling();
|
||||
setStep("fetching");
|
||||
parsePayloadMutation.mutate({ data: data.data as NormalizedImport });
|
||||
parsePayloadMutation.mutate({
|
||||
source: config.source as "trakt" | "simkl",
|
||||
rawPayload: data.data,
|
||||
});
|
||||
} else if (data.status === "denied") {
|
||||
stopPolling();
|
||||
setOauthError(t`Authorization was denied. Please try again.`);
|
||||
|
||||
Reference in New Issue
Block a user