mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
chore(deps): upgrade oRPC to v2 (2.0.0-beta.42)
- Pin all @orpc/* catalog entries to 2.0.0-beta.42; @sofa/api now depends on @orpc/openapi - Contract routes move from .route() to .meta(openapi(...)); schemas gain meta ids so the OpenAPI document hoists them as named components; publicInfo overrides document-level security - OpenAPI generator/handler use v2 APIs (converters, base, spec plugin option, OpenAPI 3.1.1 output) - Add restoreRootFileType routing interceptor so root-level File inputs (avatar upload, backup restore) keep their MIME type under Bun.serve - Web/native links use origin + path url; native error classification derives HTTP status from COMMON_ERROR_STATUS_MAP / MalformedResponseError since ORPCError.status is gone - router-auth test reads v2 orderedMiddlewares; regenerated docs/public/openapi.json Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
22 files changed
+2880
-3971
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { MalformedResponseError, ORPCError } from "@orpc/client";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.mock("@sofa/i18n", () => ({
|
||||
@@ -31,6 +31,22 @@ describe("error classification", () => {
|
||||
expect(isClientError(new ORPCError("INTERNAL_SERVER_ERROR"))).toBe(false);
|
||||
expect(isClientError(new Error("Network request failed"))).toBe(false);
|
||||
});
|
||||
|
||||
const malformed = (status: number) =>
|
||||
new ORPCError("MALFORMED_ORPC_RESPONSE", {
|
||||
cause: new MalformedResponseError({ response: { status, headers: {}, body: undefined } }),
|
||||
});
|
||||
|
||||
test("classifies non-oRPC responses by their HTTP status", () => {
|
||||
expect(isUnauthorizedError(malformed(401))).toBe(true);
|
||||
expect(isClientError(malformed(404))).toBe(true);
|
||||
expect(isClientError(malformed(502))).toBe(false);
|
||||
});
|
||||
|
||||
test("unknown error codes are not client errors", () => {
|
||||
expect(isClientError(new ORPCError("NOT_FOUND"))).toBe(true);
|
||||
expect(isClientError(new ORPCError("SOMETHING_CUSTOM"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getErrorMessage", () => {
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { COMMON_ERROR_STATUS_MAP, MalformedResponseError, ORPCError } from "@orpc/client";
|
||||
|
||||
export { getAppErrorCode, getAuthErrorMessage, getErrorMessage } from "@sofa/i18n/errors";
|
||||
|
||||
/**
|
||||
* HTTP status behind an oRPC error. oRPC v2 errors carry no `status`: well-formed errors map their
|
||||
* code through oRPC's standard table, and non-oRPC responses (a proxy's 401, or an older Sofa
|
||||
* server whose error format differs) surface as `MALFORMED_ORPC_RESPONSE` with the raw response
|
||||
* on `cause`.
|
||||
*/
|
||||
function getErrorStatus(error: ORPCError<string, unknown>): number | undefined {
|
||||
if (error.cause instanceof MalformedResponseError) return error.cause.response.status;
|
||||
return (COMMON_ERROR_STATUS_MAP as Record<string, number | undefined>)[error.code];
|
||||
}
|
||||
|
||||
/** True when an oRPC call failed because the server no longer accepts the session. */
|
||||
export function isUnauthorizedError(error: unknown): boolean {
|
||||
return error instanceof ORPCError && (error.code === "UNAUTHORIZED" || error.status === 401);
|
||||
return error instanceof ORPCError && getErrorStatus(error) === 401;
|
||||
}
|
||||
|
||||
/** True for 4xx oRPC errors — retrying them can't succeed. */
|
||||
export function isClientError(error: unknown): boolean {
|
||||
return error instanceof ORPCError && error.status >= 400 && error.status < 500;
|
||||
if (!(error instanceof ORPCError)) return false;
|
||||
const status = getErrorStatus(error);
|
||||
return status !== undefined && status >= 400 && status < 500;
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { createORPCClient } from "@orpc/client";
|
||||
import { RPCLink } from "@orpc/client/fetch";
|
||||
import type { ContractRouterClient } from "@orpc/contract";
|
||||
import type { RouterContractClient } from "@orpc/contract";
|
||||
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
||||
|
||||
import { authClient, getServerUrl, serverFetch } from "@/lib/server";
|
||||
import type { contract } from "@sofa/api/contract";
|
||||
|
||||
export const link = new RPCLink({
|
||||
url: () => `${getServerUrl()}/rpc`,
|
||||
// The server URL may carry a path prefix (reverse proxy); the link concatenates origin + url.
|
||||
origin: () => getServerUrl(),
|
||||
url: "/rpc",
|
||||
fetch: (url, options) =>
|
||||
serverFetch(url, {
|
||||
...options,
|
||||
@@ -20,12 +22,12 @@ export const link = new RPCLink({
|
||||
const headers = new Map<string, string>();
|
||||
const cookies = await authClient.getCookie();
|
||||
if (cookies) {
|
||||
headers.set("Cookie", cookies);
|
||||
headers.set("cookie", cookies);
|
||||
}
|
||||
return Object.fromEntries(headers);
|
||||
},
|
||||
});
|
||||
|
||||
export const client: ContractRouterClient<typeof contract> = createORPCClient(link);
|
||||
export const client: RouterContractClient<typeof contract> = createORPCClient(link);
|
||||
|
||||
export const orpc = createTanstackQueryUtils(client);
|
||||
@@ -3,11 +3,13 @@ import { RPCHandler } from "@orpc/server/fetch";
|
||||
|
||||
import { createLogger } from "@sofa/logger";
|
||||
|
||||
import { restoreRootFileType } from "./restore-file-type";
|
||||
import { router } from "./router";
|
||||
|
||||
const log = createLogger("orpc");
|
||||
|
||||
export const handler = new RPCHandler(router, {
|
||||
routingInterceptors: [restoreRootFileType()],
|
||||
interceptors: [
|
||||
onError((error) => {
|
||||
log.error("oRPC error", error);
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
import { oo } from "@orpc/openapi";
|
||||
import { os as baseOs, ORPCError } from "@orpc/server";
|
||||
|
||||
import { auth } from "@sofa/auth/server";
|
||||
|
||||
const base = baseOs.$context<{ headers: Headers }>();
|
||||
|
||||
export const authed = oo.spec(
|
||||
base.middleware(async ({ context, next }) => {
|
||||
const sessionData = await auth.api.getSession({
|
||||
headers: context.headers,
|
||||
});
|
||||
if (!sessionData?.session || !sessionData?.user) {
|
||||
throw new ORPCError("UNAUTHORIZED");
|
||||
}
|
||||
return next({
|
||||
context: {
|
||||
user: sessionData.user,
|
||||
session: sessionData.session,
|
||||
},
|
||||
});
|
||||
}),
|
||||
{ security: [{ session: [] }] },
|
||||
);
|
||||
export const authed = base.middleware(async ({ context, next }) => {
|
||||
const sessionData = await auth.api.getSession({
|
||||
headers: context.headers,
|
||||
});
|
||||
if (!sessionData?.session || !sessionData?.user) {
|
||||
throw new ORPCError("UNAUTHORIZED");
|
||||
}
|
||||
return next({
|
||||
context: {
|
||||
user: sessionData.user,
|
||||
session: sessionData.session,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const admin = oo.spec(
|
||||
base.middleware(async ({ context, next }) => {
|
||||
const sessionData = await auth.api.getSession({
|
||||
headers: context.headers,
|
||||
// Bypass the 5-minute cookie cache so role changes take effect immediately.
|
||||
query: { disableCookieCache: true },
|
||||
});
|
||||
if (!sessionData?.session || !sessionData?.user) {
|
||||
throw new ORPCError("UNAUTHORIZED");
|
||||
}
|
||||
if (sessionData.user.role !== "admin") {
|
||||
throw new ORPCError("FORBIDDEN");
|
||||
}
|
||||
return next({
|
||||
context: {
|
||||
user: sessionData.user,
|
||||
session: sessionData.session,
|
||||
},
|
||||
});
|
||||
}),
|
||||
{ security: [{ session: [] }] },
|
||||
);
|
||||
export const admin = base.middleware(async ({ context, next }) => {
|
||||
const sessionData = await auth.api.getSession({
|
||||
headers: context.headers,
|
||||
// Bypass the 5-minute cookie cache so role changes take effect immediately.
|
||||
query: { disableCookieCache: true },
|
||||
});
|
||||
if (!sessionData?.session || !sessionData?.user) {
|
||||
throw new ORPCError("UNAUTHORIZED");
|
||||
}
|
||||
if (sessionData.user.role !== "admin") {
|
||||
throw new ORPCError("FORBIDDEN");
|
||||
}
|
||||
return next({
|
||||
context: {
|
||||
user: sessionData.user,
|
||||
session: sessionData.session,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,14 @@
|
||||
import { SmartCoercionPlugin } from "@orpc/json-schema";
|
||||
import { SmartCoercionHandlerPlugin } from "@orpc/json-schema";
|
||||
import { OpenAPIHandler } from "@orpc/openapi/fetch";
|
||||
import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins";
|
||||
import { OpenAPIReferenceHandlerPlugin } from "@orpc/openapi/plugins";
|
||||
import { onError } from "@orpc/server";
|
||||
|
||||
import { createLogger } from "@sofa/logger";
|
||||
|
||||
import { generateOpenApiSpec, openApiTags, schemaConverters } from "./openapi-spec";
|
||||
import { restoreRootFileType } from "./restore-file-type";
|
||||
import { implementedRouter } from "./router";
|
||||
|
||||
const TRAILING_SLASH_RE = /\/$/;
|
||||
|
||||
const log = createLogger("openapi");
|
||||
|
||||
const isSecure = (process.env.BETTER_AUTH_URL ?? "").startsWith("https://");
|
||||
@@ -17,99 +16,23 @@ const sessionCookieName = isSecure
|
||||
? "__Secure-better-auth.session_token"
|
||||
: "better-auth.session_token";
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
export const openApiHandler = new OpenAPIHandler(implementedRouter, {
|
||||
plugins: [
|
||||
new SmartCoercionPlugin({ schemaConverters }),
|
||||
new OpenAPIReferencePlugin({
|
||||
schemaConverters,
|
||||
specGenerateOptions: {
|
||||
info: {
|
||||
new SmartCoercionHandlerPlugin({ converters: schemaConverters }),
|
||||
new OpenAPIReferenceHandlerPlugin({
|
||||
docsTitle: "Sofa API",
|
||||
spec: () =>
|
||||
generateOpenApiSpec({
|
||||
title: "Sofa API",
|
||||
version: process.env.APP_VERSION || "0.0.0",
|
||||
},
|
||||
servers: [{ url: "/api/v1" }],
|
||||
tags: [...openApiTags],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
session: {
|
||||
type: "apiKey",
|
||||
name: sessionCookieName,
|
||||
in: "cookie",
|
||||
description: "Better Auth session cookie",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Load the spec from /spec.json so the docs use the normalized document
|
||||
// rather than the plugin's raw generator output.
|
||||
renderDocsHtml: (specUrl, title, head, scriptUrl, config) => {
|
||||
const scalarConfig = {
|
||||
url: specUrl,
|
||||
...(typeof config === "object" && config !== null ? config : {}),
|
||||
};
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${escapeHtml(title)}</title>
|
||||
${head}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-config="${escapeHtml(JSON.stringify(scalarConfig))}"></div>
|
||||
|
||||
<script src="${escapeHtml(scriptUrl)}"></script>
|
||||
|
||||
<script>
|
||||
Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
},
|
||||
servers: [{ url: "/api/v1" }],
|
||||
sessionCookieName,
|
||||
tags: [...openApiTags],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
routingInterceptors: [restoreRootFileType()],
|
||||
interceptors: [
|
||||
async (options) => {
|
||||
const requestPathname = options.request.url.pathname.replace(TRAILING_SLASH_RE, "") || "/";
|
||||
const prefix = options.prefix?.replace(TRAILING_SLASH_RE, "") || "";
|
||||
const specPath = `${prefix}/spec.json`.replace(TRAILING_SLASH_RE, "") || "/";
|
||||
|
||||
if (options.request.method !== "GET" || requestPathname !== specPath) {
|
||||
return options.next();
|
||||
}
|
||||
|
||||
const spec = await generateOpenApiSpec({
|
||||
title: "Sofa API",
|
||||
version: process.env.APP_VERSION || "0.0.0",
|
||||
servers: [{ url: prefix || "/api/v1" }],
|
||||
sessionCookieName,
|
||||
tags: [...openApiTags],
|
||||
});
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
response: {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: new File([JSON.stringify(spec)], "spec.json", {
|
||||
type: "application/json",
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
onError((error) => {
|
||||
log.error("OpenAPI error", error);
|
||||
}),
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { OpenAPIGenerator, type OpenAPIGeneratorGenerateOptions } from "@orpc/openapi";
|
||||
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
|
||||
|
||||
import {
|
||||
BackupSchema,
|
||||
CastMemberSchema,
|
||||
EpisodeSchema,
|
||||
IntegrationEventSchema,
|
||||
IntegrationSchema,
|
||||
JobSchema,
|
||||
PersonCreditSchema,
|
||||
PersonSchema,
|
||||
RecommendationItemSchema,
|
||||
ResolvedTitleSchema,
|
||||
SeasonSchema,
|
||||
SystemHealthSchema,
|
||||
TmdbBrowseItem,
|
||||
} from "@sofa/api/schemas";
|
||||
OpenAPIGenerator,
|
||||
type OpenAPIDocument,
|
||||
type OpenAPIGeneratorGenerateOptions,
|
||||
} from "@orpc/openapi";
|
||||
import { ZodToJsonSchemaConverter } from "@orpc/zod";
|
||||
|
||||
import { implementedRouter } from "./router";
|
||||
|
||||
@@ -33,12 +21,16 @@ export const openApiTags = [
|
||||
] as const;
|
||||
|
||||
const generator = new OpenAPIGenerator({
|
||||
schemaConverters,
|
||||
converters: schemaConverters,
|
||||
});
|
||||
|
||||
const httpMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const;
|
||||
|
||||
type OpenApiSpec = Awaited<ReturnType<OpenAPIGenerator["generate"]>>;
|
||||
// oRPC v2 defaults to OpenAPI 3.2; keep emitting 3.1 like v1 did so the committed docs spec and
|
||||
// its renderers (fumadocs-openapi, Scalar) see the same dialect.
|
||||
const OPENAPI_VERSION = "3.1.1";
|
||||
|
||||
type OpenApiSpec = OpenAPIDocument<typeof OPENAPI_VERSION>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
@@ -173,13 +165,16 @@ export function normalizeOpenApiSpec<T extends OpenApiSpec>(spec: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
// ResponsesObject allows `x-*` extension keys, so its values are typed loosely.
|
||||
for (const response of Object.values(operation.responses ?? {})) {
|
||||
if (!response || !("content" in response)) continue;
|
||||
if (!isRecord(response) || !isRecord(response.content)) continue;
|
||||
|
||||
const nextContent = normalizeContent(response.content);
|
||||
const nextContent = normalizeContent(
|
||||
response.content as Record<string, { schema?: unknown }>,
|
||||
);
|
||||
|
||||
if (nextContent) {
|
||||
response.content = nextContent as typeof response.content;
|
||||
response.content = nextContent;
|
||||
} else {
|
||||
delete response.content;
|
||||
}
|
||||
@@ -193,39 +188,25 @@ export function normalizeOpenApiSpec<T extends OpenApiSpec>(spec: T): T {
|
||||
export async function generateOpenApiSpec(options: {
|
||||
title: string;
|
||||
version: string;
|
||||
servers: OpenAPIGeneratorGenerateOptions["servers"];
|
||||
servers: NonNullable<OpenAPIGeneratorGenerateOptions<typeof OPENAPI_VERSION>["base"]>["servers"];
|
||||
sessionCookieName: string;
|
||||
tags?: Array<{ name: string; description?: string }>;
|
||||
}): Promise<OpenApiSpec> {
|
||||
const spec = await generator.generate(implementedRouter, {
|
||||
info: {
|
||||
title: options.title,
|
||||
version: options.version,
|
||||
},
|
||||
servers: options.servers,
|
||||
tags: options.tags,
|
||||
commonSchemas: {
|
||||
Title: { schema: ResolvedTitleSchema },
|
||||
Person: { schema: PersonSchema },
|
||||
PersonCredit: { schema: PersonCreditSchema },
|
||||
Episode: { schema: EpisodeSchema },
|
||||
Season: { schema: SeasonSchema },
|
||||
CastMember: { schema: CastMemberSchema },
|
||||
BrowseItem: { schema: TmdbBrowseItem },
|
||||
Recommendation: { schema: RecommendationItemSchema },
|
||||
Integration: { schema: IntegrationSchema },
|
||||
IntegrationEvent: { schema: IntegrationEventSchema },
|
||||
Backup: { schema: BackupSchema },
|
||||
Job: { schema: JobSchema },
|
||||
SystemHealth: { schema: SystemHealthSchema },
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
session: {
|
||||
type: "apiKey",
|
||||
name: options.sessionCookieName,
|
||||
in: "cookie",
|
||||
description: "Better Auth session cookie",
|
||||
version: OPENAPI_VERSION,
|
||||
base: {
|
||||
info: { title: options.title, version: options.version },
|
||||
servers: options.servers,
|
||||
tags: options.tags,
|
||||
security: [{ session: [] }],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
session: {
|
||||
type: "apiKey",
|
||||
name: options.sessionCookieName,
|
||||
in: "cookie",
|
||||
description: "Better Auth session cookie",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Context } from "@orpc/server";
|
||||
import type { StandardHandlerRoutingInterceptor } from "@orpc/server/standard";
|
||||
|
||||
/**
|
||||
* Restores the MIME type of a procedure input that is a bare `File` (avatar upload, backup restore).
|
||||
*
|
||||
* oRPC v2 sends such an input as the raw request body and rebuilds the `File` from
|
||||
* `request.blob()`, whose type Bun.serve always reports as `text/plain;charset=utf-8`. Without this,
|
||||
* `z.file().mime([...])` rejects every upload. Files nested in an object arrive as multipart form
|
||||
* data and keep their type, so only a root-level `File` body is re-typed.
|
||||
*/
|
||||
export function restoreRootFileType<T extends Context>(): StandardHandlerRoutingInterceptor<T> {
|
||||
return (options) => {
|
||||
const contentType = options.request.headers["content-type"];
|
||||
if (typeof contentType !== "string") return options.next();
|
||||
|
||||
const resolveBody = options.request.resolveBody;
|
||||
return options.next({
|
||||
...options,
|
||||
request: {
|
||||
...options.request,
|
||||
resolveBody: async (hint) => {
|
||||
const body = await resolveBody(hint);
|
||||
return body instanceof File && body.type !== contentType
|
||||
? new File([body], body.name, { type: contentType, lastModified: body.lastModified })
|
||||
: body;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { restoreRootFileType } from "../src/orpc/restore-file-type";
|
||||
|
||||
async function resolveThroughInterceptor(
|
||||
headers: Record<string, string | undefined>,
|
||||
body: unknown,
|
||||
): Promise<unknown> {
|
||||
let resolved: unknown;
|
||||
await restoreRootFileType()({
|
||||
request: { url: "/rpc/x", method: "POST", headers, resolveBody: async () => body },
|
||||
next: async (options?: { request: { resolveBody: () => Promise<unknown> } }) => {
|
||||
resolved = await (
|
||||
options ?? { request: { resolveBody: async () => body } }
|
||||
).request.resolveBody();
|
||||
return { matched: false };
|
||||
},
|
||||
} as never);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
describe("restoreRootFileType", () => {
|
||||
test("re-types a root-level File from the Content-Type header", async () => {
|
||||
const wrong = new File([new Uint8Array([1, 2, 3])], "a.png", {
|
||||
type: "text/plain;charset=utf-8",
|
||||
});
|
||||
const result = await resolveThroughInterceptor({ "content-type": "image/png" }, wrong);
|
||||
expect(result).toBeInstanceOf(File);
|
||||
expect((result as File).type).toBe("image/png");
|
||||
expect((result as File).name).toBe("a.png");
|
||||
expect((result as File).size).toBe(3);
|
||||
});
|
||||
|
||||
test("leaves non-File bodies untouched", async () => {
|
||||
const json = { a: 1 };
|
||||
expect(await resolveThroughInterceptor({ "content-type": "application/json" }, json)).toBe(
|
||||
json,
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves a File alone when the type already matches", async () => {
|
||||
const file = new File(["x"], "a.png", { type: "image/png" });
|
||||
expect(await resolveThroughInterceptor({ "content-type": "image/png" }, file)).toBe(file);
|
||||
});
|
||||
|
||||
test("passes through when there is no Content-Type header", async () => {
|
||||
const file = new File(["x"], "a.bin", { type: "" });
|
||||
expect(await resolveThroughInterceptor({}, file)).toBe(file);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isProcedure } from "@orpc/server";
|
||||
import { Procedure } from "@orpc/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { admin, authed } from "../src/orpc/middleware";
|
||||
@@ -11,8 +11,13 @@ function collectProcedures(
|
||||
node: unknown,
|
||||
path: string[] = [],
|
||||
): { path: string; middlewares: readonly unknown[] }[] {
|
||||
if (isProcedure(node)) {
|
||||
return [{ path: path.join("."), middlewares: node["~orpc"].middlewares }];
|
||||
if (node instanceof Procedure) {
|
||||
return [
|
||||
{
|
||||
path: path.join("."),
|
||||
middlewares: node["~orpc"].orderedMiddlewares.map((m) => m.middleware),
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.entries(node as Record<string, unknown>).flatMap(([key, child]) =>
|
||||
collectProcedures(child, [...path, key]),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import { createORPCClient } from "@orpc/client";
|
||||
import { RPCLink } from "@orpc/client/fetch";
|
||||
import type { ContractRouterClient } from "@orpc/contract";
|
||||
import type { RouterContractClient } from "@orpc/contract";
|
||||
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
@@ -30,10 +30,11 @@ export const queryClient = new QueryClient({
|
||||
});
|
||||
|
||||
export const link = new RPCLink({
|
||||
url: `${window.location.origin}/rpc`,
|
||||
fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
|
||||
// No `origin`: in the browser the link uses the current origin.
|
||||
url: "/rpc",
|
||||
fetch: (url, init) => fetch(url, { ...init, credentials: "include" }),
|
||||
});
|
||||
|
||||
export const client: ContractRouterClient<typeof contract> = createORPCClient(link);
|
||||
export const client: RouterContractClient<typeof contract> = createORPCClient(link);
|
||||
|
||||
export const orpc = createTanstackQueryUtils(client);
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/_app")({
|
||||
let updateCheck = null;
|
||||
if (session.user.role === "admin") {
|
||||
try {
|
||||
const settings = await client.admin.settings.get({});
|
||||
const settings = await client.admin.settings.get();
|
||||
updateCheck = settings.updateCheck;
|
||||
} catch {
|
||||
// Silently ignore — update check is non-critical
|
||||
|
||||
@@ -5,7 +5,7 @@ import { client } from "@/lib/orpc/client";
|
||||
|
||||
export const Route = createFileRoute("/_auth/login")({
|
||||
beforeLoad: async () => {
|
||||
const publicInfo = await client.system.publicInfo({});
|
||||
const publicInfo = await client.system.publicInfo();
|
||||
if (publicInfo.userCount === 0) throw redirect({ to: "/register" });
|
||||
|
||||
return { publicInfo };
|
||||
|
||||
@@ -7,7 +7,7 @@ import { client } from "@/lib/orpc/client";
|
||||
|
||||
export const Route = createFileRoute("/_auth/register")({
|
||||
beforeLoad: async () => {
|
||||
const publicInfo = await client.system.publicInfo({});
|
||||
const publicInfo = await client.system.publicInfo();
|
||||
return { publicInfo };
|
||||
},
|
||||
head: () => ({ meta: [{ title: "Create account — Sofa" }] }),
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Route = createFileRoute("/")({
|
||||
beforeLoad: async () => {
|
||||
const [{ data: session }, info] = await Promise.all([
|
||||
authClient.getSession(),
|
||||
client.system.publicInfo({}),
|
||||
client.system.publicInfo(),
|
||||
]);
|
||||
if (session?.user) throw redirect({ to: "/dashboard" });
|
||||
if (!info.tmdbConfigured) throw redirect({ to: "/setup" });
|
||||
|
||||
@@ -28,7 +28,7 @@ const envSnippets = [
|
||||
|
||||
export const Route = createFileRoute("/setup")({
|
||||
beforeLoad: async () => {
|
||||
const info = await client.system.publicInfo({});
|
||||
const info = await client.system.publicInfo();
|
||||
if (info.tmdbConfigured) throw redirect({ to: "/" });
|
||||
},
|
||||
head: () => ({ meta: [{ title: "Setup — Sofa" }] }),
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@orpc/contract": "catalog:",
|
||||
"@orpc/openapi": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -363,13 +364,13 @@
|
||||
"@lingui/metro-transformer": "6.9.0",
|
||||
"@lingui/react": "6.9.0",
|
||||
"@lingui/vite-plugin": "6.9.0",
|
||||
"@orpc/client": "1.15.4",
|
||||
"@orpc/contract": "1.15.4",
|
||||
"@orpc/json-schema": "1.15.4",
|
||||
"@orpc/openapi": "1.15.4",
|
||||
"@orpc/server": "1.15.4",
|
||||
"@orpc/tanstack-query": "1.15.4",
|
||||
"@orpc/zod": "1.15.4",
|
||||
"@orpc/client": "2.0.0-beta.42",
|
||||
"@orpc/contract": "2.0.0-beta.42",
|
||||
"@orpc/json-schema": "2.0.0-beta.42",
|
||||
"@orpc/openapi": "2.0.0-beta.42",
|
||||
"@orpc/server": "2.0.0-beta.42",
|
||||
"@orpc/tanstack-query": "2.0.0-beta.42",
|
||||
"@orpc/zod": "2.0.0-beta.42",
|
||||
"@tanstack/react-form": "1.33.5",
|
||||
"@tanstack/react-query": "5.104.1",
|
||||
"@types/bun": "1.4.2",
|
||||
@@ -927,39 +928,27 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@openapi-spec/downgrader": ["@openapi-spec/downgrader@0.3.0", "", { "dependencies": { "@openapi-spec/types": "^0.3.0" } }, "sha512-y4+CkV8chSbRKmR64+uGr0IjDjNUOQIdNMpCbSiD2JCc0a0HouUTXvgt+FO8/ktfaMaNz3ypqq4PmVfB3twx3w=="],
|
||||
|
||||
"@openapi-spec/types": ["@openapi-spec/types@0.3.0", "", {}, "sha512-r9Gufbaw9Cpb/rCiEsFdmdWdj41MzL4/y6dCJjwMQBUvQqj5X76WlbErDSwIHR3UsxCNQI2W20GsQ4eh3YXaMQ=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@orpc/client": ["@orpc/client@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "@orpc/standard-server-fetch": "1.15.4", "@orpc/standard-server-peer": "1.15.4" } }, "sha512-8Iv2K8Rj8hf6xHCTxmQ+gxgiY6EATDossQk4gdt3oSbGVV0x2RSeLtRK9Zry+KVsmOqPJfnLahfRp7H2rQ/Vgg=="],
|
||||
"@orpc/client": ["@orpc/client@2.0.0-beta.42", "", { "dependencies": { "@orpc/shared": "2.0.0-beta.42", "@standard-server/core": "~0.10.1", "@standard-server/fetch": "~0.10.1", "@standard-server/peer": "~0.10.1" } }, "sha512-5UVRiHw9UsOVsfK1EweJoOqDhnseIz1TG4U/pcGe/P6I/yBDafh+ijZ/ZaBrql2hC7PSgEwfCFljEG8IfxVGqA=="],
|
||||
|
||||
"@orpc/contract": ["@orpc/contract@1.15.4", "", { "dependencies": { "@orpc/client": "1.15.4", "@orpc/shared": "1.15.4", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-mxH+kuIsKz7BGsoqDHShnMjKG7GvCMGpvxTtJnkpbowIp2iBz8XiWosiWBybZcUl1Dy9W77TyOk6pNqG5I4D6w=="],
|
||||
"@orpc/contract": ["@orpc/contract@2.0.0-beta.42", "", { "dependencies": { "@orpc/client": "2.0.0-beta.42", "@orpc/shared": "2.0.0-beta.42", "@standard-schema/spec": "^1.1.0" } }, "sha512-cLtAdjHGwGy2kNASBsdGKwlhFmRwqmlCOb9Avi6asFoV2fJH3Phht7QHa2/g61Yz1qF8IM6NgJ2+OA7iXRmU/g=="],
|
||||
|
||||
"@orpc/interop": ["@orpc/interop@1.15.4", "", {}, "sha512-ZCTNPenr1clZ2RBCJxsCsvS3eS8MJIDSx/LH1AIAKj9RjvfOXI/BnjHmgD4jUEIa6FcG+c2LV/1IXVJpC6NoTw=="],
|
||||
"@orpc/json-schema": ["@orpc/json-schema@2.0.0-beta.42", "", { "dependencies": { "@openapi-spec/types": "~0.3.0", "@orpc/client": "2.0.0-beta.42", "@orpc/contract": "2.0.0-beta.42", "@orpc/server": "2.0.0-beta.42", "@orpc/shared": "2.0.0-beta.42", "@standard-schema/spec": "^1.1.0" } }, "sha512-iw/NsGLkTS3Blqb7TUzCSITaD6DMTuKIxMyoNT3TJBId3hC5JaEm4eiKktg4xA0zWn8NdUbOcsg+1E+l7b95fg=="],
|
||||
|
||||
"@orpc/json-schema": ["@orpc/json-schema@1.15.4", "", { "dependencies": { "@orpc/contract": "1.15.4", "@orpc/interop": "1.15.4", "@orpc/openapi": "1.15.4", "@orpc/server": "1.15.4", "@orpc/shared": "1.15.4", "json-schema-typed": "^8.0.2" } }, "sha512-UIPzRBWuSkWejOJwIAGFkW8H0iOvqobGJnVGRbpKOGAw1kiJW2EqHQ+2ZhE2o/cTi/A+HnQj0YG9tmrk5Vytnw=="],
|
||||
"@orpc/openapi": ["@orpc/openapi@2.0.0-beta.42", "", { "dependencies": { "@openapi-spec/downgrader": "~0.3.0", "@openapi-spec/types": "~0.3.0", "@orpc/client": "2.0.0-beta.42", "@orpc/contract": "2.0.0-beta.42", "@orpc/json-schema": "2.0.0-beta.42", "@orpc/server": "2.0.0-beta.42", "@orpc/shared": "2.0.0-beta.42", "@standard-server/core": "~0.10.1", "@standard-server/fetch": "~0.10.1", "rou3": "^1.0.0" }, "peerDependencies": { "@scalar/api-reference": ">=1.57.2", "@types/swagger-ui": ">=5.32.0", "swagger-ui": ">=5.32.6" }, "optionalPeers": ["@scalar/api-reference", "@types/swagger-ui", "swagger-ui"] }, "sha512-u4DdwDVXvSQBDsA//IP6i+kHDUlu+6eNsI45+8YnXeGxm+4+/ePq24zZoOrZbafTM2pevXQO+aGD2wtFNdjK0w=="],
|
||||
|
||||
"@orpc/openapi": ["@orpc/openapi@1.15.4", "", { "dependencies": { "@orpc/client": "1.15.4", "@orpc/contract": "1.15.4", "@orpc/interop": "1.15.4", "@orpc/openapi-client": "1.15.4", "@orpc/server": "1.15.4", "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-+J2skkYlqcB7O9nUJ9rhV1IvisimIwxyn21nAWP/jvSg5PN/klqSO2r6XkxiAaCukw/QE5BFMbN8c0ZdLrMEnw=="],
|
||||
"@orpc/server": ["@orpc/server@2.0.0-beta.42", "", { "dependencies": { "@orpc/client": "2.0.0-beta.42", "@orpc/contract": "2.0.0-beta.42", "@orpc/shared": "2.0.0-beta.42", "@standard-server/aws-lambda": "~0.10.1", "@standard-server/core": "~0.10.1", "@standard-server/fastify": "~0.10.1", "@standard-server/fetch": "~0.10.1", "@standard-server/node": "~0.10.1", "@standard-server/peer": "~0.10.1", "cookie": "^2.0.1" }, "peerDependencies": { "crossws": ">=0.3.4", "fastify": ">=5.6.1" }, "optionalPeers": ["crossws", "fastify"] }, "sha512-gZXjfrKnYBNTgv70C3OB1ct04rbbMXOzX+sVx393YvcoEOjUr1cO8BwMBpIAQwaS42NKcoIjjo4zXF0dXOyb3Q=="],
|
||||
|
||||
"@orpc/openapi-client": ["@orpc/openapi-client@1.15.4", "", { "dependencies": { "@orpc/client": "1.15.4", "@orpc/contract": "1.15.4", "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4" } }, "sha512-z+8gRUp9bhVuA2tFlzJDCaUbLV/whZec25fmYb0VtP2N6iPfY0d7hYZvX47F/WrcuwaLCTqkbVAzK8Xb2cKF/w=="],
|
||||
"@orpc/shared": ["@orpc/shared@2.0.0-beta.42", "", { "dependencies": { "@standard-server/shared": "~0.10.1", "radash": "^12.1.1", "type-fest": "^5.3.1" } }, "sha512-jPmLnx9ZD39bXOlbCTYdopz/xN9iqGVHZnlo3QDVruRLHN0dUDzcVyfJE38DaBCJs5A6tFYRJTG3BJlO5rM/kw=="],
|
||||
|
||||
"@orpc/server": ["@orpc/server@1.15.4", "", { "dependencies": { "@orpc/client": "1.15.4", "@orpc/contract": "1.15.4", "@orpc/interop": "1.15.4", "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "@orpc/standard-server-aws-lambda": "1.15.4", "@orpc/standard-server-fastify": "1.15.4", "@orpc/standard-server-fetch": "1.15.4", "@orpc/standard-server-node": "1.15.4", "@orpc/standard-server-peer": "1.15.4", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-Ccs2Pdwql2XRtk7bzkiZohoYuUVNcffydbswhz75rdepCQt0HmKosfVhsqjAqCKDDB7c84ct7Q7c5Z/xQak7pQ=="],
|
||||
"@orpc/tanstack-query": ["@orpc/tanstack-query@2.0.0-beta.42", "", { "dependencies": { "@orpc/client": "2.0.0-beta.42", "@orpc/contract": "2.0.0-beta.42", "@orpc/openapi": "2.0.0-beta.42", "@orpc/shared": "2.0.0-beta.42" }, "peerDependencies": { "@tanstack/query-core": ">=5.80.2" } }, "sha512-yLhmDw5Y5JR4mRrdq3J+vWnzF64g4eL63PqHDewvIPu240QQCxMeZLlKY1QPTdHZhE1ytwYnUu43Z+R2G8oCwQ=="],
|
||||
|
||||
"@orpc/shared": ["@orpc/shared@1.15.4", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-syNylxXmv1WnroSDUvtYjuvu0YMAYzkLTjFPa0CggnaF3DH2YmAnRS5GGGzSPRAbhMtRAXArIoM7GVrRNKHxng=="],
|
||||
|
||||
"@orpc/standard-server": ["@orpc/standard-server@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4" } }, "sha512-woaFpUJavYPiqRQ0Eh716jOHRZ7o84mPpmEPS79bBUJNDilGEj2OBSjbHV2ez4FYdVnEnlbg+rYy7sX3PwwENA=="],
|
||||
|
||||
"@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "@orpc/standard-server-fetch": "1.15.4", "@orpc/standard-server-node": "1.15.4" } }, "sha512-Vq5nNhq/XSM3DPsI1MLEbMraP+++hb0btsS8IfdAj5eXTYvDzpTA5rZ+gMhhgnVuZvZcQbmBojTgtu41X8t4sA=="],
|
||||
|
||||
"@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "@orpc/standard-server-node": "1.15.4" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-OkBWAUQhtssTrfaV3FF8X/gdJL7otS1MvfTBuxKoRKpf6u7CQB9QpdY76OSg407N/hvN8wzSW2lSbhlKljOxkQ=="],
|
||||
|
||||
"@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4" } }, "sha512-u6zQDx/tUgNUWDQstJMbWkCKsecp9j+4WHWoPmNNuk2PyRdD6FEmvJJs23CAKa5njKu2V8aMzuuL9AW3jechuw=="],
|
||||
|
||||
"@orpc/standard-server-node": ["@orpc/standard-server-node@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4", "@orpc/standard-server-fetch": "1.15.4" } }, "sha512-is6T03n1y6nx+g9ve0ip+F1BXbnR104r3NEGVUcGmkuLCqtxqbXCCjyzJTXWwGArTdEcxDOT4cW/zx/EORGN0g=="],
|
||||
|
||||
"@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4", "@orpc/standard-server": "1.15.4" } }, "sha512-qWH7gf+8Iv2oXVIQZIQIvAyk2LYUHcj0CXF+AKF5WzHU22vJInD4FKbl8+UesVU+nT8O+BrVmOXzrQ40TqngpQ=="],
|
||||
|
||||
"@orpc/tanstack-query": ["@orpc/tanstack-query@1.15.4", "", { "dependencies": { "@orpc/shared": "1.15.4" }, "peerDependencies": { "@orpc/client": "1.15.4", "@tanstack/query-core": ">=5.80.2" } }, "sha512-oP4XuCI5AuelIhhv722sGXzZNYTH4Y6ueS4H4TtIICdG9Q6tJeiERqyEk0dm3ebh3yo+iM/NVmcRkpUrgzIZzw=="],
|
||||
|
||||
"@orpc/zod": ["@orpc/zod@1.15.4", "", { "dependencies": { "@orpc/json-schema": "1.15.4", "@orpc/openapi": "1.15.4", "@orpc/shared": "1.15.4", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.15.4", "@orpc/server": "1.15.4", "zod": ">=3.25.0" } }, "sha512-k7W5yvagfcGztACr7qpB7Yyp3iMF9BzCJX9uDH9EEouuM2gt/WzsGtkDw//LTpPPsJQerltdKrdGfaWnbrGBhQ=="],
|
||||
"@orpc/zod": ["@orpc/zod@2.0.0-beta.42", "", { "dependencies": { "@openapi-spec/types": "~0.3.0", "@orpc/json-schema": "2.0.0-beta.42" }, "peerDependencies": { "zod": ">=4.5.0" } }, "sha512-ajYbDjL0VavTSz1DGrJb/o1UrLiIvw3KGkIM45ZXKyX4bbpqnLYwurPfUA8DBP/C+UOliPqtt2x9Cf84nwA06g=="],
|
||||
|
||||
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.120.0", "", { "os": "android", "cpu": "arm" }, "sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg=="],
|
||||
|
||||
@@ -1303,6 +1292,20 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-server/aws-lambda": ["@standard-server/aws-lambda@0.10.1", "", { "dependencies": { "@standard-server/core": "0.10.1", "@standard-server/fetch": "0.10.1", "@standard-server/node": "0.10.1", "@standard-server/shared": "0.10.1" } }, "sha512-Rba4wyO9T0ta+Ze2Gu+c27vfzEjFKBqwUobtODvoo0ArZvcycY9Pr5Bf5iFcDX1NAWPFjnyEg5YQe2d+lJqu4g=="],
|
||||
|
||||
"@standard-server/core": ["@standard-server/core@0.10.1", "", { "dependencies": { "@standard-server/shared": "0.10.1" } }, "sha512-LtTjCg2x2FfbExGRdtu5J1biNVyNOfLs1/4zHAh/t0qZlbY+jIIbFYT5BPwbiYjF4o+Va5/JKjrA6uZeOQ184w=="],
|
||||
|
||||
"@standard-server/fastify": ["@standard-server/fastify@0.10.1", "", { "dependencies": { "@standard-server/core": "0.10.1", "@standard-server/node": "0.10.1" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-8up+11uSaZRNlJ+ejyLPoxgAkftVeV1XCH+afmEbsaQ8YZlLGXQlf12mHFBoS+6Mr0NqtpYn4eBmC6yTnS7llw=="],
|
||||
|
||||
"@standard-server/fetch": ["@standard-server/fetch@0.10.1", "", { "dependencies": { "@standard-server/core": "0.10.1", "@standard-server/shared": "0.10.1" } }, "sha512-YK16ZUjJNRl7BZkqsA9dimTqNgjC/pfhXVcRcox22G1x4hrzTd993JLaQSduJ79g8hEyx0LuMCkjLavumwmonQ=="],
|
||||
|
||||
"@standard-server/node": ["@standard-server/node@0.10.1", "", { "dependencies": { "@standard-server/core": "0.10.1", "@standard-server/fetch": "0.10.1", "@standard-server/shared": "0.10.1" } }, "sha512-tVH85dY/qxflrqoGDXy/BiKmzw7w8ELif8yVbIx8XRGsc1qkTere51jsoJlx3tAQN/AC9SUoGjvgcVXVS6IwYw=="],
|
||||
|
||||
"@standard-server/peer": ["@standard-server/peer@0.10.1", "", { "dependencies": { "@standard-server/core": "0.10.1", "@standard-server/shared": "0.10.1" } }, "sha512-M1CXBNKMzCSTq33bNHxvUoGSwjjw6rqA+EJ1Cv6UJZSdAN9GoXQPVkmf1qmpPIDlc+0WhXHPoMbND1pqrblMog=="],
|
||||
|
||||
"@standard-server/shared": ["@standard-server/shared@0.10.1", "", {}, "sha512-8OhXcAY13iAtEzprBFDlfOvGVLCrNnf31oiPOJ/aZaR+i2BzT8U7qsUhYU5VPbD++KKfkzsJ7hN2kDlOxJ9C6w=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||
@@ -1757,7 +1760,7 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
|
||||
|
||||
@@ -2437,8 +2440,6 @@
|
||||
|
||||
"openapi-fetch": ["openapi-fetch@0.17.0", "", { "dependencies": { "openapi-typescript-helpers": "^0.1.0" } }, "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"openapi-typescript-helpers": ["openapi-typescript-helpers@0.1.0", "", {}, "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
@@ -2655,7 +2656,7 @@
|
||||
|
||||
"rolldown": ["rolldown@1.2.12", "", { "dependencies": { "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.12", "@rolldown/binding-android-arm64": "1.2.12", "@rolldown/binding-darwin-arm64": "1.2.12", "@rolldown/binding-darwin-x64": "1.2.12", "@rolldown/binding-freebsd-x64": "1.2.12", "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", "@rolldown/binding-linux-arm64-gnu": "1.2.12", "@rolldown/binding-linux-arm64-musl": "1.2.12", "@rolldown/binding-linux-ppc64-gnu": "1.2.12", "@rolldown/binding-linux-s390x-gnu": "1.2.12", "@rolldown/binding-linux-x64-gnu": "1.2.12", "@rolldown/binding-linux-x64-musl": "1.2.12", "@rolldown/binding-openharmony-arm64": "1.2.12", "@rolldown/binding-win32-arm64-msvc": "1.2.12", "@rolldown/binding-win32-x64-msvc": "1.2.12" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
"rou3": ["rou3@1.0.0", "", {}, "sha512-ySeaQkSrua+ut9g+65I7FHqOFBaY8tV0FmccXItoq0q21se32Q3u4YiW7/RqXSosWgYJaaKwRHXy3BST9PeCNQ=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
@@ -2945,8 +2946,6 @@
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="],
|
||||
|
||||
"wildcard-match": ["wildcard-match@5.1.4", "", {}, "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
@@ -3087,8 +3086,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"@orpc/zod/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.86.0", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@react-native/codegen": "0.86.0" } }, "sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA=="],
|
||||
|
||||
"@react-native/codegen/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
+2043
-3261
File diff suppressed because it is too large.
Load diff
+7
-7
@@ -16,13 +16,13 @@
|
||||
"@lingui/metro-transformer": "6.9.0",
|
||||
"@lingui/react": "6.9.0",
|
||||
"@lingui/vite-plugin": "6.9.0",
|
||||
"@orpc/client": "1.15.4",
|
||||
"@orpc/contract": "1.15.4",
|
||||
"@orpc/json-schema": "1.15.4",
|
||||
"@orpc/openapi": "1.15.4",
|
||||
"@orpc/server": "1.15.4",
|
||||
"@orpc/tanstack-query": "1.15.4",
|
||||
"@orpc/zod": "1.15.4",
|
||||
"@orpc/client": "2.0.0-beta.42",
|
||||
"@orpc/contract": "2.0.0-beta.42",
|
||||
"@orpc/json-schema": "2.0.0-beta.42",
|
||||
"@orpc/openapi": "2.0.0-beta.42",
|
||||
"@orpc/server": "2.0.0-beta.42",
|
||||
"@orpc/tanstack-query": "2.0.0-beta.42",
|
||||
"@orpc/zod": "2.0.0-beta.42",
|
||||
"@tanstack/react-form": "1.33.5",
|
||||
"@tanstack/react-query": "5.104.1",
|
||||
"@types/bun": "1.4.2",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@orpc/contract": "catalog:",
|
||||
"@orpc/openapi": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+562
-452
File diff suppressed because it is too large.
Load diff
+13
-11
@@ -369,7 +369,7 @@ export const EpisodeSchema = z
|
||||
airDate: z.string().nullable().describe("Original air date (ISO 8601)"),
|
||||
runtimeMinutes: z.number().nullable().describe("Episode runtime in minutes"),
|
||||
})
|
||||
.meta({ description: "A single TV episode" });
|
||||
.meta({ id: "Episode", description: "A single TV episode" });
|
||||
|
||||
export const SeasonSchema = z
|
||||
.object({
|
||||
@@ -378,7 +378,7 @@ export const SeasonSchema = z
|
||||
name: z.string().nullable().describe("Season name"),
|
||||
episodes: z.array(EpisodeSchema).describe("Episodes in this season"),
|
||||
})
|
||||
.meta({ description: "A TV season with its episodes" });
|
||||
.meta({ id: "Season", description: "A TV season with its episodes" });
|
||||
|
||||
export const AvailabilityOfferSchema = z
|
||||
.object({
|
||||
@@ -434,7 +434,7 @@ export const CastMemberSchema = z
|
||||
.describe("ThumbHash blur placeholder for the profile photo"),
|
||||
tmdbId: z.number().describe("TMDB person ID"),
|
||||
})
|
||||
.meta({ description: "A cast or crew member credit" });
|
||||
.meta({ id: "CastMember", description: "A cast or crew member credit" });
|
||||
|
||||
export const ResolvedTitleSchema = z
|
||||
.object({
|
||||
@@ -466,7 +466,7 @@ export const ResolvedTitleSchema = z
|
||||
trailerVideoKey: z.string().nullable().describe("YouTube video key for the trailer"),
|
||||
genres: z.array(z.string()).describe("Genre names"),
|
||||
})
|
||||
.meta({ description: "A fully resolved movie or TV show from TMDB" });
|
||||
.meta({ id: "Title", description: "A fully resolved movie or TV show from TMDB" });
|
||||
|
||||
export const PersonSchema = z
|
||||
.object({
|
||||
@@ -488,7 +488,7 @@ export const PersonSchema = z
|
||||
.describe("Primary department (e.g. Acting, Directing)"),
|
||||
imdbId: z.string().nullable().describe("IMDb person ID (e.g. nm0000123)"),
|
||||
})
|
||||
.meta({ description: "A person (actor, director, crew member) from TMDB" });
|
||||
.meta({ id: "Person", description: "A person (actor, director, crew member) from TMDB" });
|
||||
|
||||
export const PersonCreditSchema = z
|
||||
.object({
|
||||
@@ -505,7 +505,7 @@ export const PersonCreditSchema = z
|
||||
department: z.string().describe("Department (e.g. Acting, Directing)"),
|
||||
job: z.string().nullable().describe("Job title (for crew credits)"),
|
||||
})
|
||||
.meta({ description: "A person's credit in a movie or TV show" });
|
||||
.meta({ id: "PersonCredit", description: "A person's credit in a movie or TV show" });
|
||||
|
||||
/** Reusable TMDB browse result (trending / popular / discover items) */
|
||||
export const TmdbBrowseItem = z
|
||||
@@ -521,6 +521,7 @@ export const TmdbBrowseItem = z
|
||||
voteAverage: z.number().nullable().describe("Average rating (0-10)"),
|
||||
})
|
||||
.meta({
|
||||
id: "BrowseItem",
|
||||
description: "A TMDB title card used in browse/trending/popular lists",
|
||||
});
|
||||
|
||||
@@ -537,7 +538,7 @@ export const RecommendationItemSchema = z
|
||||
firstAirDate: z.string().nullable().describe("First air date (ISO 8601)"),
|
||||
voteAverage: z.number().nullable().describe("Average rating (0-10)"),
|
||||
})
|
||||
.meta({ description: "A recommended title" });
|
||||
.meta({ id: "Recommendation", description: "A recommended title" });
|
||||
|
||||
const displayStatusEnum = z.enum(["in_watchlist", "watching", "caught_up", "completed"]);
|
||||
|
||||
@@ -913,7 +914,7 @@ export const JobSchema = z
|
||||
isCurrentlyRunning: z.boolean().describe("Whether the job is currently executing"),
|
||||
disabled: z.boolean().describe("Whether the job is disabled"),
|
||||
})
|
||||
.meta({ description: "Status of a background cron job" });
|
||||
.meta({ id: "Job", description: "Status of a background cron job" });
|
||||
|
||||
export const SystemHealthSchema = z
|
||||
.object({
|
||||
@@ -977,6 +978,7 @@ export const SystemHealthSchema = z
|
||||
checkedAt: z.string().describe("When this health check was performed (ISO 8601)"),
|
||||
})
|
||||
.meta({
|
||||
id: "SystemHealth",
|
||||
description:
|
||||
"Comprehensive system health report covering database, TMDB, jobs, cache, backups, and environment",
|
||||
});
|
||||
@@ -1005,7 +1007,7 @@ export const IntegrationSchema = z
|
||||
lastEventAt: z.string().nullable().describe("Last received event timestamp (ISO 8601)"),
|
||||
createdAt: z.string().describe("When the integration was created (ISO 8601)"),
|
||||
})
|
||||
.meta({ description: "A media server integration configuration" });
|
||||
.meta({ id: "Integration", description: "A media server integration configuration" });
|
||||
|
||||
export const IntegrationEventSchema = z
|
||||
.object({
|
||||
@@ -1016,7 +1018,7 @@ export const IntegrationEventSchema = z
|
||||
status: z.enum(["success", "ignored", "error"]).describe("Event processing outcome"),
|
||||
receivedAt: z.string().describe("When the event was received (ISO 8601)"),
|
||||
})
|
||||
.meta({ description: "A webhook or sync event from a media server" });
|
||||
.meta({ id: "IntegrationEvent", description: "A webhook or sync event from a media server" });
|
||||
|
||||
export const IntegrationsListOutput = z
|
||||
.object({
|
||||
@@ -1045,7 +1047,7 @@ export const BackupSchema = z
|
||||
.enum(["manual", "scheduled", "pre-restore"])
|
||||
.describe("How the backup was created: manual, scheduled, or automatic pre-restore"),
|
||||
})
|
||||
.meta({ description: "A database backup file" });
|
||||
.meta({ id: "Backup", description: "A database backup file" });
|
||||
|
||||
export const BackupsListOutput = z
|
||||
.object({
|
||||
|
||||
Reference in new issue
Block a user