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:
2026-03-23 12:48:04 -04:00
parent 9df1075040
commit cc8909d64e
19 changed files with 250 additions and 396 deletions
+3 -3
View File
@@ -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(
+7 -3
View File
@@ -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;
} {
+5 -10
View File
@@ -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;
};
},
};
+5 -11
View File
@@ -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 -5
View File
@@ -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>;
}