Skip to content

Commit

Permalink
fix: search engine name autogeneration
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Dec 30, 2024
1 parent 799fc67 commit 2a3c1b8
Show file tree
Hide file tree
Showing 12 changed files with 3,358 additions and 10 deletions.
38 changes: 33 additions & 5 deletions packages/api/src/router/integration/integration-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,14 @@ export const integrationRouter = createTRPCRouter({
}

if (input.attemptSearchEngineCreation) {
const icon = await getIconForNameAsync(input.name);
const icon = await getIconForNameAsync(ctx.db, input.name);
await ctx.db.insert(searchEngines).values({
id: createId(),
name: input.name,
integrationId,
type: "fromIntegration",
iconUrl:
icon?.url ??
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg",
short: input.name.substring(0, 1),
iconUrl: icon?.url ?? "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg",
short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
});
}
}),
Expand Down Expand Up @@ -427,6 +425,36 @@ interface AddSecretInput {
value: string;
kind: IntegrationSecretKind;
}

const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
const searchEngines = await db.query.searchEngines.findMany({
columns: {
short: true,
},
});

const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short);
const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();

if (!usedShortNames.includes(nameByIntegrationName)) {
return nameByIntegrationName;
}

// 8 is max length constraint
for (let i = 2; i < 9999999; i++) {
const generatedName = `${nameByIntegrationName}${i}`;
if (usedShortNames.includes(generatedName)) {
continue;
}

return generatedName;
}

throw new Error(
"Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
);
};

const addSecretAsync = async (db: Database, input: AddSecretInput) => {
await db.insert(integrationSecrets).values({
kind: input.kind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ describe("create should create a new integration", () => {
kind: "jellyfin" as const,
url: "http://jellyfin.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: false,
};

const fakeNow = new Date("2023-07-01T00:00:00Z");
Expand All @@ -201,6 +202,46 @@ describe("create should create a new integration", () => {
expect(dbSecret!.updatedAt).toEqual(fakeNow);
});

test("with create integration access should create a new integration when creating search engine", async () => {
const db = createDb();
const caller = integrationRouter.createCaller({
db,
session: defaultSessionWithPermissions(["integration-create"]),
});
const input = {
name: "Jellyseerr",
kind: "jellyseerr" as const,
url: "http://jellyseerr.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: true,
};

const fakeNow = new Date("2023-07-01T00:00:00Z");
vi.useFakeTimers();
vi.setSystemTime(fakeNow);
await caller.create(input);
vi.useRealTimers();

const dbIntegration = await db.query.integrations.findFirst();
const dbSecret = await db.query.integrationSecrets.findFirst();
const dbSearchEngine = await db.query.searchEngines.findFirst();
expect(dbIntegration).toBeDefined();
expect(dbIntegration!.name).toBe(input.name);
expect(dbIntegration!.kind).toBe(input.kind);
expect(dbIntegration!.url).toBe(input.url);

expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
expect(dbSecret).toBeDefined();
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
expect(dbSecret!.updatedAt).toEqual(fakeNow);

expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id);
expect(dbSearchEngine!.short).toBe("j");
expect(dbSearchEngine!.name).toBe(input.name);
expect(dbSearchEngine!.iconUrl).toBe("");

Check failure on line 242 in packages/api/src/router/test/integration/integration-router.spec.ts

View workflow job for this annotation

GitHub Actions / test

packages/api/src/router/test/integration/integration-router.spec.ts > create should create a new integration > with create integration access should create a new integration when creating search engine

AssertionError: expected 'https://cdn.jsdelivr.net/gh/walkxcode…' to be '' // Object.is equality - Expected + Received + https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg ❯ packages/api/src/router/test/integration/integration-router.spec.ts:242:37
});

test("without create integration access should throw permission error", async () => {
// Arrange
const db = createDb();
Expand All @@ -213,6 +254,7 @@ describe("create should create a new integration", () => {
kind: "jellyfin" as const,
url: "http://jellyfin.local",
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
attemptSearchEngineCreation: false,
};

// Act
Expand Down
1 change: 1 addition & 0 deletions packages/db/migrations/mysql/0018_mighty_shaman.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `search_engine` ADD CONSTRAINT `search_engine_short_unique` UNIQUE(`short`);
Loading

0 comments on commit 2a3c1b8

Please sign in to comment.