feat: finish the homepage plugin, move service links and canvas out of core (B19)

This commit is contained in:
LukeGus committed 2026-09-24 15:07:25 -05:00
1 parent 21937a6b02
commit d4ec444558
256 files changed
+27365 -9077

No files matched your search

@@ -1,137 +0,0 @@
import { and, asc, eq } from "drizzle-orm";
import { randomUUID } from "crypto";
import { dashboardServiceLinks } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import {
deleteReturning,
insertReturning,
updateReturning,
} from "./returning.js";
export type DashboardServiceLinkRecord =
typeof dashboardServiceLinks.$inferSelect;
export type DashboardServiceLinkUpdate = Partial<{
label: string;
url: string;
}>;
export class DashboardServiceLinkRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async listByUserId(userId: string): Promise<DashboardServiceLinkRecord[]> {
return this.context.drizzle
.select()
.from(dashboardServiceLinks)
.where(eq(dashboardServiceLinks.userId, userId))
.orderBy(asc(dashboardServiceLinks.order), asc(dashboardServiceLinks.id));
}
async createForUser(
userId: string,
input: { label: string; url: string },
createdAt = new Date().toISOString(),
): Promise<DashboardServiceLinkRecord> {
const existing = await this.context.drizzle
.select({ order: dashboardServiceLinks.order })
.from(dashboardServiceLinks)
.where(eq(dashboardServiceLinks.userId, userId))
.orderBy(asc(dashboardServiceLinks.order));
const nextOrder =
existing.length > 0 ? existing[existing.length - 1].order + 1 : 0;
const [created] = await insertReturning(
this.context,
dashboardServiceLinks,
{
syncId: randomUUID(),
userId,
label: input.label,
url: input.url,
order: nextOrder,
createdAt,
updatedAt: createdAt,
},
);
await this.afterWrite();
return created;
}
async findByIdForUser(
userId: string,
id: number,
): Promise<DashboardServiceLinkRecord | null> {
const rows = await this.context.drizzle
.select()
.from(dashboardServiceLinks)
.where(
and(
eq(dashboardServiceLinks.id, id),
eq(dashboardServiceLinks.userId, userId),
),
)
.limit(1);
return rows[0] ?? null;
}
async updateForUser(
userId: string,
id: number,
updates: DashboardServiceLinkUpdate,
): Promise<DashboardServiceLinkRecord | null> {
const [updated] = await updateReturning(
this.context,
dashboardServiceLinks,
{ ...updates, updatedAt: new Date().toISOString() },
and(
eq(dashboardServiceLinks.id, id),
eq(dashboardServiceLinks.userId, userId),
),
);
if (updated) {
await this.afterWrite();
}
return updated ?? null;
}
async deleteForUser(
userId: string,
id: number,
): Promise<{ syncId: string | null } | null> {
const rows = await deleteReturning(
this.context,
dashboardServiceLinks,
and(
eq(dashboardServiceLinks.id, id),
eq(dashboardServiceLinks.userId, userId),
),
);
if (rows.length === 0) return null;
await this.afterWrite();
return { syncId: rows[0].syncId };
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(dashboardServiceLinks)
.where(eq(dashboardServiceLinks.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
+1 -24
View File
@@ -8,10 +8,7 @@ import { NotificationChannelRepository } from "./notification-channel-repository
import { ApiKeyRepository } from "./api-key-repository.js";
import { AuditLogRepository } from "./audit-log-repository.js";
import { CredentialRepository } from "./credential-repository.js";
import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js";
import { DismissedAlertRepository } from "./dismissed-alert-repository.js";
import { HomepageItemRepository } from "./homepage-item-repository.js";
import { HomepageLayoutRepository } from "./homepage-layout-repository.js";
import { HostFolderRepository } from "./host-folder-repository.js";
import { HostRepository } from "./host-repository.js";
import { HostResolutionRepository } from "./host-resolution-repository.js";
@@ -167,13 +164,6 @@ export function createCurrentCredentialRepository(): CredentialRepository {
);
}
export function createCurrentDashboardServiceLinkRepository(): DashboardServiceLinkRepository {
return new DashboardServiceLinkRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("dashboard_service_link_repository_write"),
);
}
export function createCurrentSyncTombstoneRepository(): SyncTombstoneRepository {
return new SyncTombstoneRepository(
createCurrentRepositoryContext(),
@@ -189,20 +179,7 @@ export function createCurrentDismissedAlertRepository(): DismissedAlertRepositor
}
// File manager recent/pinned/shortcuts moved to the file-manager plugin.
export function createCurrentHomepageItemRepository(): HomepageItemRepository {
return new HomepageItemRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("homepage_item_repository_write"),
);
}
export function createCurrentHomepageLayoutRepository(): HomepageLayoutRepository {
return new HomepageLayoutRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("homepage_layout_repository_write"),
);
}
// Homepage items/layout and dashboard service links moved to the homepage plugin.
export function createCurrentHostFolderRepository(): HostFolderRepository {
return new HostFolderRepository(
@@ -1,121 +0,0 @@
import { and, asc, eq } from "drizzle-orm";
import { randomUUID } from "crypto";
import { homepageItems } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import {
deleteReturning,
insertReturning,
updateReturning,
} from "./returning.js";
export type HomepageItemRecord = typeof homepageItems.$inferSelect;
export interface HomepageItemCreateInput {
typeId: string;
title: string | null;
config: string;
}
export type HomepageItemUpdateInput = Partial<{
title: string | null;
config: string;
}>;
export class HomepageItemRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async listByUserId(userId: string): Promise<HomepageItemRecord[]> {
return this.context.drizzle
.select()
.from(homepageItems)
.where(eq(homepageItems.userId, userId))
.orderBy(asc(homepageItems.id));
}
async createForUser(
userId: string,
input: HomepageItemCreateInput,
now = new Date().toISOString(),
): Promise<HomepageItemRecord> {
const [created] = await insertReturning(this.context, homepageItems, {
syncId: randomUUID(),
userId,
typeId: input.typeId,
title: input.title,
config: input.config,
createdAt: now,
updatedAt: now,
});
await this.afterWrite();
return created;
}
async findByIdForUser(
userId: string,
id: number,
): Promise<HomepageItemRecord | null> {
const rows = await this.context.drizzle
.select()
.from(homepageItems)
.where(and(eq(homepageItems.id, id), eq(homepageItems.userId, userId)))
.limit(1);
return rows[0] ?? null;
}
async updateForUser(
userId: string,
id: number,
updates: HomepageItemUpdateInput,
updatedAt = new Date().toISOString(),
): Promise<HomepageItemRecord | null> {
const [updated] = await updateReturning(
this.context,
homepageItems,
{ ...updates, updatedAt },
and(eq(homepageItems.id, id), eq(homepageItems.userId, userId)),
);
if (updated) {
await this.afterWrite();
}
return updated ?? null;
}
async deleteForUser(
userId: string,
id: number,
): Promise<{ syncId: string | null } | null> {
const rows = await deleteReturning(
this.context,
homepageItems,
and(eq(homepageItems.id, id), eq(homepageItems.userId, userId)),
);
if (rows.length === 0) return null;
await this.afterWrite();
return { syncId: rows[0].syncId };
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(homepageItems)
.where(eq(homepageItems.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
@@ -1,67 +0,0 @@
import { eq } from "drizzle-orm";
import { homepageLayouts } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import { insertReturning, updateReturning } from "./returning.js";
export type HomepageLayoutRecord = typeof homepageLayouts.$inferSelect;
export class HomepageLayoutRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async findByUserId(userId: string): Promise<HomepageLayoutRecord | null> {
const rows = await this.context.drizzle
.select()
.from(homepageLayouts)
.where(eq(homepageLayouts.userId, userId))
.limit(1);
return rows[0] ?? null;
}
async upsertForUser(
userId: string,
layout: string,
updatedAt = new Date().toISOString(),
): Promise<HomepageLayoutRecord> {
const existing = await this.findByUserId(userId);
if (!existing) {
const [created] = await insertReturning(this.context, homepageLayouts, {
userId,
layout,
updatedAt,
});
await this.afterWrite();
return created;
}
const [updated] = await updateReturning(
this.context,
homepageLayouts,
{ layout, updatedAt },
eq(homepageLayouts.userId, userId),
);
await this.afterWrite();
return updated;
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(homepageLayouts)
.where(eq(homepageLayouts.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}