feat: add onboarding with oldmarr import (#1606)

This commit is contained in:
Meier Lukas
2024-12-15 15:40:26 +01:00
committed by GitHub
parent 82ec77d2da
commit 6de74d9525
108 changed files with 6045 additions and 312 deletions

View File

@@ -0,0 +1,78 @@
import { createId } from "@homarr/db";
import { logger } from "@homarr/log";
import { fixSectionIssues } from "../../fix-section-issues";
import { mapBoard } from "../../mappers/map-board";
import { moveWidgetsAndAppsIfMerge } from "../../move-widgets-and-apps-merge";
import { prepareItems } from "../../prepare/prepare-items";
import type { prepareMultipleImports } from "../../prepare/prepare-multiple";
import { prepareSections } from "../../prepare/prepare-sections";
import type { InitialOldmarrImportSettings } from "../../settings";
import { createDbInsertCollection } from "./common";
export const createBoardInsertCollection = (
{ preparedApps, preparedBoards }: Omit<ReturnType<typeof prepareMultipleImports>, "preparedIntegrations">,
settings: InitialOldmarrImportSettings,
) => {
const insertCollection = createDbInsertCollection(["apps", "boards", "sections", "items"]);
logger.info("Preparing boards for insert collection");
const appsMap = new Map(
preparedApps.flatMap(({ ids, ...app }) => {
const id = app.existingId ?? createId();
return ids.map((oldId) => [oldId, { id, ...app }] as const);
}),
);
for (const app of appsMap.values()) {
// Skip duplicate apps
if (insertCollection.apps.some((appEntry) => appEntry.id === app.id)) {
continue;
}
// Skip apps that already exist in the database
if (app.existingId) {
continue;
}
insertCollection.apps.push(app);
}
if (settings.onlyImportApps) {
logger.info(
`Skipping boards and sections import due to onlyImportApps setting appCount=${insertCollection.apps.length}`,
);
return insertCollection;
}
logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`);
preparedBoards.forEach((board) => {
const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config);
const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, {
...settings,
screenSize: board.size,
name: board.name,
});
logger.debug(`Fixed issues with sections and item positions fileName=${board.name}`);
const mappedBoard = mapBoard(board);
logger.debug(`Mapped board fileName=${board.name} boardId=${mappedBoard.id}`);
insertCollection.boards.push(mappedBoard);
const preparedSections = prepareSections(mappedBoard.id, { wrappers, categories });
for (const section of preparedSections.values()) {
insertCollection.sections.push(section);
}
logger.debug(`Added sections to board insert collection count=${insertCollection.sections.length}`);
const preparedItems = prepareItems({ apps, widgets }, board.size, appsMap, preparedSections);
preparedItems.forEach((item) => insertCollection.items.push(item));
logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`);
});
logger.info(
`Board collection prepared boardCount=${insertCollection.boards.length} sectionCount=${insertCollection.sections.length} itemCount=${insertCollection.items.length} appCount=${insertCollection.apps.length}`,
);
return insertCollection;
};

View File

@@ -0,0 +1,33 @@
import { objectEntries } from "@homarr/common";
import type { Database, InferInsertModel } from "@homarr/db";
import { schema } from "@homarr/db";
type TableKey = {
[K in keyof typeof schema]: (typeof schema)[K] extends { _: { brand: "Table" } } ? K : never;
}[keyof typeof schema];
export const createDbInsertCollection = <TTableKey extends TableKey>(tablesInInsertOrder: TTableKey[]) => {
const context = tablesInInsertOrder.reduce(
(acc, key) => {
acc[key] = [];
return acc;
},
{} as { [K in TTableKey]: InferInsertModel<(typeof schema)[K]>[] },
);
return {
...context,
insertAll: (db: Database) => {
db.transaction((transaction) => {
for (const [key, values] of objectEntries(context)) {
if (values.length >= 1) {
transaction
.insert(schema[key])
.values(values as never)
.run();
}
}
});
},
};
};

View File

@@ -0,0 +1,47 @@
import { encryptSecret } from "@homarr/common/server";
import { logger } from "@homarr/log";
import { mapAndDecryptIntegrations } from "../../mappers/map-integration";
import type { PreparedIntegration } from "../../prepare/prepare-integrations";
import { createDbInsertCollection } from "./common";
export const createIntegrationInsertCollection = (
preparedIntegrations: PreparedIntegration[],
encryptionToken: string | null,
) => {
const insertCollection = createDbInsertCollection(["integrations", "integrationSecrets"]);
logger.info(`Preparing integrations for insert collection count=${preparedIntegrations.length}`);
if (encryptionToken === null) {
logger.debug("Skipping integration decryption due to missing token");
return insertCollection;
}
const preparedIntegrationsDecrypted = mapAndDecryptIntegrations(preparedIntegrations, encryptionToken);
preparedIntegrationsDecrypted.forEach((integration) => {
insertCollection.integrations.push({
id: integration.id,
kind: integration.kind,
name: integration.name,
url: integration.url,
});
integration.secrets
.filter((secret) => secret.value !== null)
.forEach((secret) => {
insertCollection.integrationSecrets.push({
integrationId: integration.id,
kind: secret.field,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: encryptSecret(secret.value!),
});
});
});
logger.info(
`Added integrations and secrets to insert collection integrationCount=${insertCollection.integrations.length} secretCount=${insertCollection.integrationSecrets.length}`,
);
return insertCollection;
};

View File

@@ -0,0 +1,53 @@
import { createId } from "@homarr/db";
import { credentialsAdminGroup } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { mapAndDecryptUsers } from "../../mappers/map-user";
import type { OldmarrImportUser } from "../../user-schema";
import { createDbInsertCollection } from "./common";
export const createUserInsertCollection = (importUsers: OldmarrImportUser[], encryptionToken: string | null) => {
const insertCollection = createDbInsertCollection(["users", "groups", "groupMembers", "groupPermissions"]);
logger.info(`Preparing users for insert collection count=${importUsers.length}`);
if (encryptionToken === null) {
logger.debug("Skipping user decryption due to missing token");
return insertCollection;
}
const preparedUsers = mapAndDecryptUsers(importUsers, encryptionToken);
preparedUsers.forEach((user) => insertCollection.users.push(user));
logger.debug(`Added users to insert collection count=${insertCollection.users.length}`);
if (!preparedUsers.some((user) => user.isAdmin)) {
logger.warn("No admin users found, skipping admin group creation");
return insertCollection;
}
const adminGroupId = createId();
insertCollection.groups.push({
id: adminGroupId,
name: credentialsAdminGroup,
});
insertCollection.groupPermissions.push({
groupId: adminGroupId,
permission: "admin",
});
const admins = preparedUsers.filter((user) => user.isAdmin);
admins.forEach((user) => {
insertCollection.groupMembers.push({
groupId: adminGroupId,
userId: user.id,
});
});
logger.info(
`Added admin group and permissions to insert collection adminGroupId=${adminGroupId} adminUsersCount=${admins.length}`,
);
return insertCollection;
};