Skip to content

Commit

Permalink
fix: import configuration missing and apps not imported
Browse files Browse the repository at this point in the history
  • Loading branch information
Meierschlumpf committed Sep 23, 2024
1 parent d580bdd commit 6b0b733
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 31 deletions.
140 changes: 119 additions & 21 deletions packages/old-import/src/import-apps.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
import { createId, inArray } from "@homarr/db";
import type { Database, InferInsertModel } from "@homarr/db";
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
import { apps as appsTable } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { OldmarrApp } from "@homarr/old-schema";

import type { BookmarkApp } from "./widgets/definitions/bookmark";

type DbAppWithoutId = Omit<InferSelectModel<typeof appsTable>, "id">;

interface AppMapping extends DbAppWithoutId {
ids: string[];
newId: string;
exists: boolean;
}

export const insertAppsAsync = async (
db: Database,
apps: OldmarrApp[],
bookmarkApps: BookmarkApp[],
distinctAppsByHref: boolean,
configName: string,
) => {
logger.info(
`Importing old homarr apps configuration=${configName} distinctAppsByHref=${distinctAppsByHref} apps=${apps.length}`,
);

const existingAppsWithHref = distinctAppsByHref
? await db.query.apps.findMany({
where: inArray(appsTable.href, [...new Set(apps.map((app) => app.url))]),
where: inArray(appsTable.href, [
...new Set(apps.map((app) => app.url).concat(bookmarkApps.map((app) => app.href))),
]),
})
: [];

logger.debug(`Found existing apps with href count=${existingAppsWithHref.length}`);

const mappedApps = apps.map((app) => ({
// Use id of existing app when it has the same href and distinctAppsByHref is true
newId: distinctAppsByHref
? (existingAppsWithHref.find(
(existingApp) =>
existingApp.href === (app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl) &&
existingApp.name === app.name &&
existingApp.iconUrl === app.appearance.iconUrl,
)?.id ?? createId())
: createId(),
...app,
}));

const appsToCreate = mappedApps
.filter((app) => !existingAppsWithHref.some((existingApp) => existingApp.id === app.newId))
// Generate mappings for all apps from old to new ids
const appMappings: AppMapping[] = [];
addMappingFor(apps, appMappings, existingAppsWithHref, convertApp);
addMappingFor(bookmarkApps, appMappings, existingAppsWithHref, convertBookmarkApp);

logger.debug(`Mapping apps count=${appMappings.length}`);

const appsToCreate = appMappings
.filter((app) => !app.exists)
.map(
(app) =>
({
id: app.newId,
name: app.name,
iconUrl: app.appearance.iconUrl,
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
description: app.behaviour.tooltipDescription,
iconUrl: app.iconUrl,
href: app.href,
description: app.description,
}) satisfies InferInsertModel<typeof appsTable>,
);

Expand All @@ -55,5 +63,95 @@ export const insertAppsAsync = async (

logger.info(`Imported apps count=${appsToCreate.length}`);

return mappedApps;
// Generates a map from old key to new key for all apps
return new Map(
appMappings
.map((app) => app.ids.map((id) => ({ id, newId: app.newId })))
.flat()
.map(({ id, newId }) => [id, newId]),
);
};

/**
* Creates a callback to be used in a find method that compares the old app with the new app
* @param app either an oldmarr app or a bookmark app
* @param convertApp a function that converts the app to a new app
* @returns a callback that compares the old app with the new app and returns true if they are the same
*/
const createFindCallback = <TApp extends OldmarrApp | BookmarkApp>(
app: TApp,
convertApp: (app: TApp) => DbAppWithoutId,
) => {
const oldApp = convertApp(app);

return (dbApp: DbAppWithoutId) =>
oldApp.href === dbApp.href &&
oldApp.name === dbApp.name &&
oldApp.iconUrl === dbApp.iconUrl &&
oldApp.description === dbApp.description;
};

/**
* Adds mappings for the given apps to the appMappings array
* @param apps apps to add mappings for
* @param appMappings existing app mappings
* @param existingAppsWithHref existing apps with href
* @param convertApp a function that converts the app to a new app
*/
const addMappingFor = <TApp extends OldmarrApp | BookmarkApp>(
apps: TApp[],
appMappings: AppMapping[],
existingAppsWithHref: InferSelectModel<typeof appsTable>[],
convertApp: (app: TApp) => DbAppWithoutId,
) => {
for (const app of apps) {
const previous = appMappings.find(createFindCallback(app, convertApp));
if (previous) {
previous.ids.push(app.id);
continue;
}

const existing = existingAppsWithHref.find(createFindCallback(app, convertApp));
if (existing) {
appMappings.push({
ids: [app.id],
newId: existing.id,
name: existing.name,
href: existing.href,
iconUrl: existing.iconUrl,
description: existing.description,
exists: true,
});
continue;
}

appMappings.push({
ids: [app.id],
newId: createId(),
...convertApp(app),
exists: false,
});
}
};

/**
* Converts an oldmarr app to a new app
* @param app oldmarr app
* @returns new app
*/
const convertApp = (app: OldmarrApp): DbAppWithoutId => ({
name: app.name,
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
iconUrl: app.appearance.iconUrl,
description: app.behaviour.tooltipDescription ?? null,
});

/**
* Converts a bookmark app to a new app
* @param app bookmark app
* @returns new app
*/
const convertBookmarkApp = (app: BookmarkApp): DbAppWithoutId => ({
...app,
description: null,
});
13 changes: 8 additions & 5 deletions packages/old-import/src/import-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { mapOptions } from "./widgets/options";
export const insertItemsAsync = async (
db: Database,
widgets: OldmarrWidget[],
mappedApps: (OldmarrApp & { newId: string })[],
apps: OldmarrApp[],
appsMap: Map<string, string>,
sectionIdMaps: Map<string, string>,
configuration: OldmarrImportConfiguration,
) => {
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${mappedApps.length}`);
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${apps.length}`);

for (const widget of widgets) {
// All items should have been moved to the last wrapper
Expand Down Expand Up @@ -54,13 +55,13 @@ export const insertItemsAsync = async (
xOffset: screenSizeShape.location.x,
yOffset: screenSizeShape.location.y,
kind,
options: SuperJSON.stringify(mapOptions(kind, widget.properties)),
options: SuperJSON.stringify(mapOptions(kind, widget.properties, appsMap)),
});

logger.debug(`Inserted widget id=${widget.id} sectionId=${sectionId}`);
}

for (const app of mappedApps) {
for (const app of apps) {
// All items should have been moved to the last wrapper
if (app.area.type === "sidebar") {
continue;
Expand All @@ -85,7 +86,9 @@ export const insertItemsAsync = async (
yOffset: screenSizeShape.location.y,
kind: "app",
options: SuperJSON.stringify({
appId: app.newId,
// it's safe to assume that the app exists in the map
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
appId: appsMap.get(app.id)!,
openInNewTab: app.behaviour.isOpeningNewTab,
pingEnabled: app.network.enabledStatusChecker,
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",
Expand Down
19 changes: 16 additions & 3 deletions packages/old-import/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ import { OldHomarrImportError, OldHomarrScreenSizeError } from "./import-error";
import { insertItemsAsync } from "./import-items";
import { insertSectionsAsync } from "./import-sections";
import { moveWidgetsAndAppsIfMerge } from "./move-widgets-and-apps-merge";
import type { BookmarkApp } from "./widgets/definitions/bookmark";

export const importAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
const bookmarkApps = old.widgets
.filter((widget) => widget.type === "bookmark")
.map((widget) => widget.properties.items)
.flat() as BookmarkApp[];

if (configuration.onlyImportApps) {
await db
.transaction(async (trasaction) => {
await insertAppsAsync(trasaction, old.apps, configuration.distinctAppsByHref, old.configProperties.name);
await insertAppsAsync(
trasaction,
old.apps,
bookmarkApps,
configuration.distinctAppsByHref,
old.configProperties.name,
);
})
.catch((error) => {
throw new OldHomarrImportError(old, error);
Expand All @@ -29,13 +41,14 @@ export const importAsync = async (db: Database, old: OldmarrConfig, configuratio

const boardId = await insertBoardAsync(trasaction, old, configuration);
const sectionIdMaps = await insertSectionsAsync(trasaction, categories, wrappers, boardId);
const mappedApps = await insertAppsAsync(
const appsMap = await insertAppsAsync(
trasaction,
apps,
bookmarkApps,
configuration.distinctAppsByHref,
old.configProperties.name,
);
await insertItemsAsync(trasaction, widgets, mappedApps, sectionIdMaps, configuration);
await insertItemsAsync(trasaction, widgets, apps, appsMap, sectionIdMaps, configuration);
})
.catch((error) => {
if (error instanceof OldHomarrScreenSizeError) {
Expand Down
2 changes: 2 additions & 0 deletions packages/old-import/src/widgets/definitions/bookmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
layout: "autoGrid" | "horizontal" | "vertical";
}
>;

export type BookmarkApp = OldmarrBookmarkDefinition["options"]["items"][number];
1 change: 1 addition & 0 deletions packages/old-import/src/widgets/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const widgetKindMapping = {
"mediaRequests-requestList": "media-requests-list",
"mediaRequests-requestStats": "media-requests-stats",
indexerManager: "indexer-manager",
bookmarks: "bookmark",
} satisfies Record<WidgetKind, OldmarrWidgetDefinitions["id"] | null>;
// Use null for widgets that did not exist in oldmarr
// TODO: revert assignment so that only old widgets are needed in the object,
Expand Down
12 changes: 11 additions & 1 deletion packages/old-import/src/widgets/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type OptionMapping = {
: {
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[WidgetKey] }>["options"],
appsMap: Map<string, string>,
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
};
};
Expand All @@ -22,6 +23,13 @@ const optionMapping: OptionMapping = {
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
},
"mediaRequests-requestStats": {},
bookmarks: {
title: (oldOptions) => oldOptions.name,
// It's safe to assume that the app exists, because the app is always created before the widget
// And the mapping is created in insertAppsAsync
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items: (oldOptions, appsMap) => oldOptions.items.map((item) => appsMap.get(item.id)!),
},
calendar: {
filterFutureMonths: () => undefined,
filterPastMonths: () => undefined,
Expand Down Expand Up @@ -110,11 +118,13 @@ const optionMapping: OptionMapping = {
* Maps the oldmarr options to the newmarr options
* @param kind item kind to map
* @param oldOptions oldmarr options for this item
* @param appsMap map of old app ids to new app ids
* @returns newmarr options for this item or null if the item did not exist in oldmarr
*/
export const mapOptions = <K extends WidgetKind>(
kind: K,
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[K] }>["options"],
appsMap: Map<string, string>,
) => {
logger.debug(`Mapping old homarr options for widget kind=${kind} options=${JSON.stringify(oldOptions)}`);
if (optionMapping[kind] === null) {
Expand All @@ -124,7 +134,7 @@ export const mapOptions = <K extends WidgetKind>(
const mapping = optionMapping[kind];
return objectEntries(mapping).reduce(
(acc, [key, value]) => {
const newValue = value(oldOptions as never);
const newValue = value(oldOptions as never, appsMap);
logger.debug(`Mapping old homarr option kind=${kind} key=${key as string} newValue=${newValue as string}`);
if (newValue !== undefined) {
acc[key as string] = newValue;
Expand Down
4 changes: 3 additions & 1 deletion packages/widgets/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ interface MultiSelectInput<TOptions extends SelectOption[]>
searchable?: boolean;
}

interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier> extends CommonInput<TOptionValue[]> {
interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier>
extends Omit<CommonInput<TOptionValue[]>, "withDescription"> {
addButton: (props: { addItem: (item: TItem) => void; values: TOptionValue[] }) => React.ReactNode;
itemComponent: (props: {
item: TItem;
Expand Down Expand Up @@ -134,6 +135,7 @@ const optionsFactory = {
addButton: input.addButton,
uniqueIdentifier: input.uniqueIdentifier,
useData: input.useData,
withDescription: false,
}),
};

Expand Down

0 comments on commit 6b0b733

Please sign in to comment.