Revert "fix: harden automatic mobile boards"
This reverts commit d0b9348b86.
This commit is contained in:
1 parent
92a2be2b65
commit
dba9a250ff
37 files changed
+137
-637
No files matched your search
@@ -5,10 +5,10 @@ import { and, eq, inArray } from "@homarr/db";
|
||||
import {
|
||||
getServerSettingByKeyAsync,
|
||||
getServerSettingsAsync,
|
||||
mergeServerSettingByKeyAsync,
|
||||
insertServerSettingByKeyAsync,
|
||||
updateServerSettingByKeyAsync,
|
||||
} from "@homarr/db/queries";
|
||||
import { boards } from "@homarr/db/schema";
|
||||
import { boards, serverSettings } from "@homarr/db/schema";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||
@@ -82,9 +82,17 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
|
||||
const current = await getServerSettingByKeyAsync(ctx.db, "board");
|
||||
const next = { ...current, ...input };
|
||||
await mergeServerSettingByKeyAsync(ctx.db, "board", next, input);
|
||||
const existing = await ctx.db.query.serverSettings.findFirst({
|
||||
where: eq(serverSettings.settingKey, "board"),
|
||||
});
|
||||
|
||||
return await getServerSettingByKeyAsync(ctx.db, "board");
|
||||
if (existing) {
|
||||
await updateServerSettingByKeyAsync(ctx.db, "board", next);
|
||||
} else {
|
||||
await insertServerSettingByKeyAsync(ctx.db, "board", next);
|
||||
}
|
||||
|
||||
return next;
|
||||
}),
|
||||
saveSettings: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -100,7 +108,15 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
...current,
|
||||
...input.value,
|
||||
} as ServerSettings[keyof ServerSettings];
|
||||
await mergeServerSettingByKeyAsync(ctx.db, input.settingsKey, next, input.value);
|
||||
const existing = await ctx.db.query.serverSettings.findFirst({
|
||||
where: eq(serverSettings.settingKey, input.settingsKey),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await updateServerSettingByKeyAsync(ctx.db, input.settingsKey, next);
|
||||
} else {
|
||||
await insertServerSettingByKeyAsync(ctx.db, input.settingsKey, next);
|
||||
}
|
||||
}),
|
||||
initSettings: onboardingProcedure
|
||||
.requiresStep("settings")
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { boards, layouts, serverSettings } from "@homarr/db/schema";
|
||||
import { boards, serverSettings } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
|
||||
@@ -116,33 +116,6 @@ describe("saveSettings", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("saveSettings should preserve concurrent partial updates", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSession,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
caller.saveSettings({
|
||||
settingsKey: "analytics",
|
||||
value: { enableGeneral: false },
|
||||
}),
|
||||
caller.saveSettings({
|
||||
settingsKey: "analytics",
|
||||
value: { instanceId: "concurrent-instance" },
|
||||
}),
|
||||
]);
|
||||
|
||||
await expect(caller.getAll()).resolves.toMatchObject({
|
||||
analytics: {
|
||||
enableGeneral: false,
|
||||
instanceId: "concurrent-instance",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("board settings API", () => {
|
||||
@@ -164,12 +137,6 @@ describe("board settings API", () => {
|
||||
deviceType: undefined,
|
||||
session: defaultSession,
|
||||
});
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({ id: boardId, name: "legacy", isPublic: false });
|
||||
await db.insert(layouts).values([
|
||||
{ id: createId(), boardId, name: "Base", columnCount: 12, breakpoint: 0 },
|
||||
{ id: createId(), boardId, name: "Mobile", columnCount: 4, breakpoint: 480 },
|
||||
]);
|
||||
const legacyBoardSettings = {
|
||||
homeBoardId: defaultServerSettings.board.homeBoardId,
|
||||
mobileHomeBoardId: defaultServerSettings.board.mobileHomeBoardId,
|
||||
@@ -181,10 +148,7 @@ describe("board settings API", () => {
|
||||
value: stringify(legacyBoardSettings),
|
||||
});
|
||||
|
||||
await expect(caller.getBoardSettings()).resolves.toStrictEqual({
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: false,
|
||||
});
|
||||
await expect(caller.getBoardSettings()).resolves.toStrictEqual(defaultServerSettings.board);
|
||||
});
|
||||
|
||||
test("updateBoardSettings should insert settings when defaults are not persisted", async () => {
|
||||
@@ -249,25 +213,6 @@ describe("board settings API", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("updateBoardSettings should preserve concurrent partial updates", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSession,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
caller.updateBoardSettings({ forceDisableStatus: true }),
|
||||
caller.updateBoardSettings({ enableStatusByDefault: false }),
|
||||
]);
|
||||
|
||||
await expect(caller.getBoardSettings()).resolves.toMatchObject({
|
||||
forceDisableStatus: true,
|
||||
enableStatusByDefault: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("updateBoardSettings should reject private home boards", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueriesWithProvenance } from "../../settle-integrations";
|
||||
import { getIntegrationQueryProvenance, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const mediaReleasesProcedure = publicProcedure.concat(
|
||||
@@ -12,7 +12,7 @@ const mediaReleasesProcedure = publicProcedure.concat(
|
||||
type MediaReleaseIntegration = Parameters<typeof mediaReleaseRequestHandler.handler>[0];
|
||||
|
||||
const getMediaReleasesWithProvenanceAsync = async (integrations: MediaReleaseIntegration[]) => {
|
||||
const { results, ...provenance } = await settleIntegrationQueriesWithProvenance(
|
||||
const results = await settleIntegrationQueries(
|
||||
integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = mediaReleaseRequestHandler.handler(integration, {});
|
||||
@@ -40,7 +40,7 @@ const getMediaReleasesWithProvenanceAsync = async (integrations: MediaReleaseInt
|
||||
|
||||
return {
|
||||
items,
|
||||
...provenance,
|
||||
...getIntegrationQueryProvenance(integrations.length, results),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
||||
|
||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueriesWithProvenance } from "../../settle-integrations";
|
||||
import { getIntegrationQueryProvenance, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
const latestRequestsProcedure = publicProcedure
|
||||
@@ -28,7 +28,7 @@ const getLatestRequestsWithProvenanceAsync = async (
|
||||
integrations: MediaRequestIntegration[],
|
||||
input: MediaRequestListInput,
|
||||
) => {
|
||||
const { results, ...provenance } = await settleIntegrationQueriesWithProvenance(
|
||||
const results = await settleIntegrationQueries(
|
||||
integrations,
|
||||
async (integration) => {
|
||||
const { data, isStale } = await mediaRequestListRequestHandler
|
||||
@@ -61,12 +61,12 @@ const getLatestRequestsWithProvenanceAsync = async (
|
||||
|
||||
return {
|
||||
items,
|
||||
...provenance,
|
||||
...getIntegrationQueryProvenance(integrations.length, results),
|
||||
};
|
||||
};
|
||||
|
||||
const getRequestStatsWithProvenanceAsync = async (integrations: MediaRequestStatsIntegration[]) => {
|
||||
const { results, ...provenance } = await settleIntegrationQueriesWithProvenance(
|
||||
const results = await settleIntegrationQueries(
|
||||
integrations,
|
||||
async (integration) => {
|
||||
const { data, isStale } = await mediaRequestStatsRequestHandler
|
||||
@@ -92,7 +92,7 @@ const getRequestStatsWithProvenanceAsync = async (integrations: MediaRequestStat
|
||||
)
|
||||
.toSorted(({ requestCount: countA }, { requestCount: countB }) => countB - countA),
|
||||
integrations: results.map((result) => result.integration),
|
||||
...provenance,
|
||||
...getIntegrationQueryProvenance(integrations.length, results),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { settleIntegrationQueries, settleIntegrationQueriesWithProvenance } from "./settle-integrations";
|
||||
import { getIntegrationQueryProvenance, settleIntegrationQueries } from "./settle-integrations";
|
||||
|
||||
const integrations = [
|
||||
{ id: "first", name: "First", kind: "mock" },
|
||||
@@ -77,23 +77,18 @@ describe("settleIntegrationQueries", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("settleIntegrationQueriesWithProvenance", () => {
|
||||
it("counts failed queries even when fallbacks preserve the result count", async () => {
|
||||
const { results, ...provenance } = await settleIntegrationQueriesWithProvenance(
|
||||
integrations,
|
||||
vi.fn(async (integration) => {
|
||||
if (integration.id === "first") throw new Error("offline");
|
||||
return { integrationId: integration.id, isStale: true };
|
||||
}),
|
||||
{
|
||||
fallback: (integration) => ({ integrationId: integration.id, isStale: false }),
|
||||
},
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(provenance).toEqual({
|
||||
describe("getIntegrationQueryProvenance", () => {
|
||||
it("counts dropped and stale integration results independently", () => {
|
||||
expect(getIntegrationQueryProvenance(4, [{ isStale: false }, { isStale: true }, { isStale: true }])).toEqual({
|
||||
failedIntegrationCount: 1,
|
||||
staleIntegrationCount: 1,
|
||||
staleIntegrationCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not report failures for complete current results", () => {
|
||||
expect(getIntegrationQueryProvenance(2, [{ isStale: false }, {}])).toEqual({
|
||||
failedIntegrationCount: 0,
|
||||
staleIntegrationCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,24 +11,13 @@ interface IntegrationLike {
|
||||
|
||||
interface Options<TIntegration extends IntegrationLike, TResult> {
|
||||
fallback?: (integration: TIntegration, error: unknown) => TResult;
|
||||
throwOnAllFailure?: boolean;
|
||||
}
|
||||
|
||||
export interface IntegrationQueryProvenance {
|
||||
failedIntegrationCount: number;
|
||||
staleIntegrationCount: number;
|
||||
}
|
||||
|
||||
interface IntegrationQuerySettlement<TResult> {
|
||||
results: TResult[];
|
||||
failedIntegrationCount: number;
|
||||
}
|
||||
|
||||
const settleIntegrationQueriesInternal = async <TIntegration extends IntegrationLike, TResult>(
|
||||
export async function settleIntegrationQueries<TIntegration extends IntegrationLike, TResult>(
|
||||
integrations: TIntegration[],
|
||||
fn: (integration: TIntegration) => Promise<TResult>,
|
||||
options?: Options<TIntegration, TResult>,
|
||||
): Promise<IntegrationQuerySettlement<TResult>> => {
|
||||
): Promise<TResult[]> {
|
||||
const settled = await Promise.allSettled(integrations.map(async (integration) => fn(integration)));
|
||||
const results: TResult[] = [];
|
||||
const errors: unknown[] = [];
|
||||
@@ -56,46 +45,9 @@ const settleIntegrationQueriesInternal = async <TIntegration extends Integration
|
||||
errors.push(result.reason);
|
||||
});
|
||||
|
||||
const firstFailure = settled.find((result) => result.status === "rejected");
|
||||
if (
|
||||
options?.throwOnAllFailure === true &&
|
||||
settled.length > 0 &&
|
||||
settled.every((result) => result.status === "rejected") &&
|
||||
firstFailure?.status === "rejected"
|
||||
) {
|
||||
throw firstFailure.reason;
|
||||
}
|
||||
|
||||
if (options?.throwOnAllFailure !== false && results.length === 0 && errors.length > 0) {
|
||||
if (results.length === 0 && errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
failedIntegrationCount: settled.filter((result) => result.status === "rejected").length,
|
||||
};
|
||||
};
|
||||
|
||||
export async function settleIntegrationQueries<TIntegration extends IntegrationLike, TResult>(
|
||||
integrations: TIntegration[],
|
||||
fn: (integration: TIntegration) => Promise<TResult>,
|
||||
options?: Options<TIntegration, TResult>,
|
||||
): Promise<TResult[]> {
|
||||
return (await settleIntegrationQueriesInternal(integrations, fn, options)).results;
|
||||
}
|
||||
|
||||
export async function settleIntegrationQueriesWithProvenance<
|
||||
TIntegration extends IntegrationLike,
|
||||
TResult extends { isStale?: boolean },
|
||||
>(
|
||||
integrations: TIntegration[],
|
||||
fn: (integration: TIntegration) => Promise<TResult>,
|
||||
options?: Options<TIntegration, TResult>,
|
||||
): Promise<IntegrationQuerySettlement<TResult> & IntegrationQueryProvenance> {
|
||||
const settlement = await settleIntegrationQueriesInternal(integrations, fn, options);
|
||||
|
||||
return {
|
||||
...settlement,
|
||||
staleIntegrationCount: settlement.results.filter((result) => result.isStale === true).length,
|
||||
};
|
||||
return results;
|
||||
}
|
||||
Reference in new issue
Block a user