refactor: tighten TypeScript boundaries
This commit is contained in:
1 parent
ad0120fe01
commit
24e6e4a47d
38 files changed
+246
-190
No files matched your search
@@ -47,10 +47,11 @@ const getProcedureInput = (inputs: z.ZodType[]) => {
|
||||
};
|
||||
|
||||
const normalizeJsonSchema = (schema: z.core.JSONSchema.JSONSchema): z.core.JSONSchema.JSONSchema => {
|
||||
const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
||||
const required = ((schema.required as string[] | undefined) ?? []).filter(
|
||||
(key) => !("default" in (properties[key] ?? {})),
|
||||
);
|
||||
const properties = schema.properties ?? {};
|
||||
const required = (schema.required ?? []).filter((key) => {
|
||||
const property = properties[key];
|
||||
return !property || typeof property !== "object" || !("default" in property);
|
||||
});
|
||||
|
||||
return {
|
||||
...schema,
|
||||
|
||||
@@ -32,6 +32,14 @@ interface IntegrationAccessMetadata {
|
||||
allowedIntegrationKinds: readonly IntegrationKind[];
|
||||
}
|
||||
|
||||
const throwUnexpectedIntegrationKind = (kind: IntegrationKind, allowedKinds: readonly IntegrationKind[]): never => {
|
||||
logger.error("Database returned an integration outside the requested kinds", {
|
||||
integrationKind: kind,
|
||||
allowedIntegrationKinds: allowedKinds,
|
||||
});
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Integration kind validation failed" });
|
||||
};
|
||||
|
||||
const createOneIntegrationMiddlewareForKinds = <TKind extends IntegrationKind>(
|
||||
action: IntegrationAction,
|
||||
kinds: readonly TKind[],
|
||||
@@ -75,21 +83,19 @@ const createOneIntegrationMiddlewareForKinds = <TKind extends IntegrationKind>(
|
||||
resolvedIntegrationKinds: [integration.kind],
|
||||
});
|
||||
|
||||
const {
|
||||
secrets,
|
||||
kind,
|
||||
items: _ignore1,
|
||||
groupPermissions: _ignore2,
|
||||
userPermissions: _ignore3,
|
||||
...rest
|
||||
} = integration;
|
||||
const narrowedKind = kinds.find((kind) => kind === integration.kind);
|
||||
if (!narrowedKind) {
|
||||
return throwUnexpectedIntegrationKind(integration.kind, kinds);
|
||||
}
|
||||
|
||||
const { secrets, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest } = integration;
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
integration: {
|
||||
...rest,
|
||||
externalUrl: rest.app?.href ?? null,
|
||||
kind: kind as TKind,
|
||||
kind: narrowedKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
@@ -176,15 +182,19 @@ const createManyIntegrationMiddlewareForKinds = <TKind extends IntegrationKind>(
|
||||
return next({
|
||||
ctx: {
|
||||
integrations: dbIntegrations.map(
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||
...rest,
|
||||
externalUrl: rest.app?.href ?? null,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
}),
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => {
|
||||
const narrowedKind = kinds.find((allowedKind) => allowedKind === kind);
|
||||
if (!narrowedKind) return throwUnexpectedIntegrationKind(kind, kinds);
|
||||
return {
|
||||
...rest,
|
||||
externalUrl: rest.app?.href ?? null,
|
||||
kind: narrowedKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -212,13 +222,19 @@ export const createOneWidgetIntegrationMiddleware = <TWidgetKind extends WidgetK
|
||||
action: IntegrationAction,
|
||||
widgetKind: TWidgetKind,
|
||||
): ReturnType<typeof createOneIntegrationMiddlewareForKinds<WidgetIntegrationKind<TWidgetKind>>> =>
|
||||
createOneIntegrationMiddlewareForKinds(action, getWidgetIntegrationConfig(widgetKind).supportedIntegrations);
|
||||
createOneIntegrationMiddlewareForKinds<WidgetIntegrationKind<TWidgetKind>>(
|
||||
action,
|
||||
getWidgetIntegrationConfig(widgetKind).supportedIntegrations,
|
||||
);
|
||||
|
||||
export const createManyWidgetIntegrationMiddleware = <TWidgetKind extends WidgetKindWithIntegration>(
|
||||
action: IntegrationAction,
|
||||
widgetKind: TWidgetKind,
|
||||
): ReturnType<typeof createManyIntegrationMiddlewareForKinds<WidgetIntegrationKind<TWidgetKind>>> =>
|
||||
createManyIntegrationMiddlewareForKinds(action, getWidgetIntegrationConfig(widgetKind).supportedIntegrations);
|
||||
createManyIntegrationMiddlewareForKinds<WidgetIntegrationKind<TWidgetKind>>(
|
||||
action,
|
||||
getWidgetIntegrationConfig(widgetKind).supportedIntegrations,
|
||||
);
|
||||
|
||||
/**
|
||||
* Throws a TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
|
||||
@@ -43,7 +43,7 @@ export const calendarRouter = createTRPCRouter({
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,23 +11,36 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const firewallMiddleware = createManyWidgetIntegrationMiddleware("query", "firewall");
|
||||
|
||||
const queryFirewall = <
|
||||
THandler extends {
|
||||
handler: (integration: any, input: any) => { getDataAsync: () => Promise<{ data: any; timestamp: Date }> };
|
||||
},
|
||||
>(
|
||||
handler: THandler,
|
||||
createFallback: () => Awaited<ReturnType<ReturnType<THandler["handler"]>["getDataAsync"]>>["data"],
|
||||
) =>
|
||||
type FirewallIntegration = Parameters<typeof firewallCpuRequestHandler.handler>[0];
|
||||
|
||||
interface FirewallHandler<TData> {
|
||||
handler: (
|
||||
integration: FirewallIntegration,
|
||||
input: Record<string, never>,
|
||||
) => { getDataAsync: () => Promise<{ data: TData; timestamp: Date }> };
|
||||
}
|
||||
|
||||
interface FirewallQueryResult<TData> {
|
||||
integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: FirewallIntegration["kind"];
|
||||
updatedAt: Date;
|
||||
};
|
||||
summary: TData;
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
const queryFirewall = <TData>(handler: FirewallHandler<TData>, createFallback: () => TData) =>
|
||||
publicProcedure.concat(firewallMiddleware).query(async ({ ctx }) =>
|
||||
settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
async (integration): Promise<FirewallQueryResult<TData>> => {
|
||||
const { data, timestamp } = await handler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
|
||||
summary: data as Awaited<ReturnType<ReturnType<THandler["handler"]>["getDataAsync"]>>["data"],
|
||||
error: undefined as string | undefined,
|
||||
summary: data,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ export const indexerManagerRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
indexers,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ export const mediaOrganizerRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
...data,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
@@ -98,8 +98,8 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
const { data } = await mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data: { ...data, stats: data.stats as typeof data.stats | null },
|
||||
error: undefined as string | undefined,
|
||||
data,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
integrationName: integration.name,
|
||||
integrationKind: integration.kind,
|
||||
sessions: data,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ export const notificationsRouter = createTRPCRouter({
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
data,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,9 +18,9 @@ export const speedtestTrackerRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,9 +18,9 @@ export const tracearrRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,9 +25,9 @@ export const traefikRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ export const upsRouter = createTRPCRouter({
|
||||
integrationUrl: integration.url,
|
||||
summaries: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,9 +18,9 @@ export const uptimeKumaRouter = createTRPCRouter({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,8 +19,8 @@ export const vpnRouter = createTRPCRouter({
|
||||
kind: integration.kind,
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
summary: data as typeof data | null,
|
||||
error: undefined as string | undefined,
|
||||
summary: data,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,18 +15,18 @@ interface IntegrationLike {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface Options<TIntegration extends IntegrationLike, TResult> {
|
||||
fallback?: (integration: TIntegration, error: unknown) => TResult;
|
||||
interface Options<TIntegration extends IntegrationLike, TFallback> {
|
||||
fallback?: (integration: TIntegration, error: unknown) => TFallback;
|
||||
throwOnAllFailures?: boolean;
|
||||
}
|
||||
|
||||
export async function settleIntegrationQueries<TIntegration extends IntegrationLike, TResult>(
|
||||
export async function settleIntegrationQueries<TIntegration extends IntegrationLike, TResult, TFallback = TResult>(
|
||||
integrations: TIntegration[],
|
||||
fn: (integration: TIntegration) => Promise<TResult>,
|
||||
options?: Options<TIntegration, TResult>,
|
||||
): Promise<TResult[]> {
|
||||
options?: Options<TIntegration, TFallback>,
|
||||
): Promise<(TResult | TFallback)[]> {
|
||||
const settled = await Promise.allSettled(integrations.map(async (integration) => fn(integration)));
|
||||
const results: TResult[] = [];
|
||||
const results: (TResult | TFallback)[] = [];
|
||||
const errors: unknown[] = [];
|
||||
|
||||
settled.forEach((result, index) => {
|
||||
|
||||
Reference in new issue
Block a user