diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 8978459cf..377002605 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,31 +1,19 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "packageRules": [ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["config:recommended"], + packageRules: [ { - "matchPackagePatterns": [ - "^@homarr/", - "tsx" // Disabled for now as version 0.14.4 did not work with the current version of homarr. It resulted in a ERR_MODULE_NOT_FOUND error - ], - "enabled": false + matchPackagePatterns: ["^@homarr/"], + enabled: false, }, { - "matchUpdateTypes": [ - "minor", - "patch", - "pin", - "digest" - ], - "automerge": true - } + matchUpdateTypes: ["minor", "patch", "pin", "digest"], + automerge: true, + }, ], - "updateInternalDeps": true, - "rangeStrategy": "bump", - "automerge": false, - "baseBranches": [ - "dev" - ], - "dependencyDashboard": false -} \ No newline at end of file + updateInternalDeps: true, + rangeStrategy: "bump", + automerge: false, + baseBranches: ["dev"], + dependencyDashboard: false, +} diff --git a/apps/nextjs/src/app/[locale]/manage/layout.tsx b/apps/nextjs/src/app/[locale]/manage/layout.tsx index e0bd9fcb5..43e11f3a3 100644 --- a/apps/nextjs/src/app/[locale]/manage/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/layout.tsx @@ -25,6 +25,7 @@ import { import { auth } from "@homarr/auth/next"; import { isProviderEnabled } from "@homarr/auth/server"; +import { createDocumentationLink } from "@homarr/definitions"; import { getScopedI18n } from "@homarr/translation/server"; import { MainHeader } from "~/components/layout/header"; @@ -124,7 +125,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) { { label: t("items.help.items.documentation"), icon: IconBook2, - href: "https://homarr.dev/docs/getting-started/", + href: createDocumentationLink("/docs/getting-started"), external: true, }, { diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx index f5241161f..5f71808d4 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_rename-group-form.tsx @@ -15,9 +15,10 @@ interface RenameGroupFormProps { id: string; name: string; }; + disabled?: boolean; } -export const RenameGroupForm = ({ group }: RenameGroupFormProps) => { +export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => { const t = useI18n(); const { mutate, isPending } = clientApi.group.updateGroup.useMutation(); const form = useZodForm(validation.group.update.pick({ name: true }), { @@ -28,6 +29,9 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => { const handleSubmit = useCallback( (values: FormType) => { + if (disabled) { + return; + } mutate( { ...values, @@ -60,13 +64,15 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => { return (
- + - - - + {!disabled && ( + + + + )}
); diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_reserved-group-alert.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_reserved-group-alert.tsx new file mode 100644 index 000000000..bd6920792 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_reserved-group-alert.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import { Alert, Anchor } from "@mantine/core"; +import { IconExclamationCircle } from "@tabler/icons-react"; + +import { createDocumentationLink } from "@homarr/definitions"; +import { getI18n } from "@homarr/translation/server"; + +export const ReservedGroupAlert = async () => { + const t = await getI18n(); + + return ( + }> + {t("group.reservedNotice.message", { + checkoutDocs: ( + + {t("common.action.checkoutDocs")} + + ), + })} + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx index 972b62d99..d20f285a8 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/members/page.tsx @@ -6,9 +6,11 @@ import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; import { env } from "@homarr/auth/env.mjs"; import { isProviderEnabled } from "@homarr/auth/server"; +import { everyoneGroup } from "@homarr/definitions"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { SearchInput, UserAvatar } from "@homarr/ui"; +import { ReservedGroupAlert } from "../_reserved-group-alert"; import { AddGroupMember } from "./_add-group-member"; import { RemoveGroupMember } from "./_remove-group-member"; @@ -25,6 +27,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD const t = await getI18n(); const tMembers = await getScopedI18n("management.page.group.setting.members"); const group = await api.group.getById({ id: params.id }); + const isReserved = group.name === everyoneGroup; const filteredMembers = searchParams.search ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -41,15 +44,19 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD {tMembers("title")} - {providerTypes !== "credentials" && ( - }> - {t(`group.memberNotice.${providerTypes}`)} - + {isReserved ? ( + + ) : ( + providerTypes !== "credentials" && ( + }> + {t(`group.memberNotice.${providerTypes}`)} + + ) )} - {isProviderEnabled("credentials") && ( + {isProviderEnabled("credentials") && !isReserved && ( member.id)} /> )} @@ -63,7 +70,7 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD {filteredMembers.map((member) => ( - + ))}
@@ -74,9 +81,10 @@ export default async function GroupsDetailPage({ params, searchParams }: GroupsD interface RowProps { member: RouterOutputs["group"]["getById"]["members"][number]; groupId: string; + disabled?: boolean; } -const Row = ({ member, groupId }: RowProps) => { +const Row = ({ member, groupId, disabled }: RowProps) => { return ( @@ -88,7 +96,7 @@ const Row = ({ member, groupId }: RowProps) => { - {member.provider === "credentials" && } + {member.provider === "credentials" && !disabled && } ); diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/page.tsx index cc3496520..455a835e0 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/page.tsx @@ -1,11 +1,13 @@ import { Stack, Title } from "@mantine/core"; import { api } from "@homarr/api/server"; +import { everyoneGroup } from "@homarr/definitions"; import { getScopedI18n } from "@homarr/translation/server"; import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"; import { DeleteGroup } from "./_delete-group"; import { RenameGroupForm } from "./_rename-group-form"; +import { ReservedGroupAlert } from "./_reserved-group-alert"; import { TransferGroupOwnership } from "./_transfer-group-ownership"; interface GroupsDetailPageProps { @@ -18,26 +20,31 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps const group = await api.group.getById({ id: params.id }); const tGeneral = await getScopedI18n("management.page.group.setting.general"); const tGroupAction = await getScopedI18n("group.action"); + const isReserved = group.name === everyoneGroup; return ( {tGeneral("title")} - + {isReserved && } - - } - /> + - } - /> - + {!isReserved && ( + + } + /> + + } + /> + + )} ); } diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 6b843a392..3eb638eb7 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -48,7 +48,7 @@ "dotenv-cli": "^7.4.2", "eslint": "^9.13.0", "prettier": "^3.3.3", - "tsx": "4.13.3", + "tsx": "4.19.1", "typescript": "^5.6.3" } } diff --git a/apps/tasks/src/main.ts b/apps/tasks/src/main.ts index 4ae066828..2cb395ad2 100644 --- a/apps/tasks/src/main.ts +++ b/apps/tasks/src/main.ts @@ -4,10 +4,7 @@ import "./undici-log-agent-override"; import { registerCronJobRunner } from "@homarr/cron-job-runner"; import { jobGroup } from "@homarr/cron-jobs"; -import { seedServerSettingsAsync } from "./seed-server-settings"; - void (async () => { registerCronJobRunner(); await jobGroup.startAllAsync(); - await seedServerSettingsAsync(); })(); diff --git a/apps/tasks/src/seed-server-settings.ts b/apps/tasks/src/seed-server-settings.ts deleted file mode 100644 index 885d4e64c..000000000 --- a/apps/tasks/src/seed-server-settings.ts +++ /dev/null @@ -1,28 +0,0 @@ -import SuperJSON from "superjson"; - -import { db } from "@homarr/db"; -import { serverSettings } from "@homarr/db/schema/sqlite"; -import { logger } from "@homarr/log"; - -import { defaultServerSettings, defaultServerSettingsKeys } from "../../../packages/server-settings"; - -export const seedServerSettingsAsync = async () => { - const serverSettingsData = await db.query.serverSettings.findMany(); - let insertedSettingsCount = 0; - - for (const settingsKey of defaultServerSettingsKeys) { - if (serverSettingsData.some((setting) => setting.settingKey === settingsKey)) { - return; - } - - await db.insert(serverSettings).values({ - settingKey: settingsKey, - value: SuperJSON.stringify(defaultServerSettings[settingsKey]), - }); - insertedSettingsCount++; - } - - if (insertedSettingsCount > 0) { - logger.info(`Inserted ${insertedSettingsCount} missing settings`); - } -}; diff --git a/apps/websocket/package.json b/apps/websocket/package.json index 9b28c5191..bf522cfdf 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -26,7 +26,7 @@ "@homarr/redis": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "dotenv": "^16.4.5", - "tsx": "4.13.3", + "tsx": "4.19.1", "ws": "^8.18.0" }, "devDependencies": { diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index d14083995..8726d9fd7 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -3,6 +3,7 @@ import { TRPCError } from "@trpc/server"; import type { Database } from "@homarr/db"; import { and, createId, eq, like, not, sql } from "@homarr/db"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite"; +import { everyoneGroup } from "@homarr/definitions"; import { validation, z } from "@homarr/validation"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc"; @@ -121,13 +122,12 @@ export const groupRouter = createTRPCRouter({ .requiresPermission("admin") .input(validation.group.create) .mutation(async ({ input, ctx }) => { - const normalizedName = normalizeName(input.name); - await checkSimilarNameAndThrowAsync(ctx.db, normalizedName); + await checkSimilarNameAndThrowAsync(ctx.db, input.name); const id = createId(); await ctx.db.insert(groups).values({ id, - name: normalizedName, + name: input.name, ownerId: ctx.session.user.id, }); @@ -138,14 +138,14 @@ export const groupRouter = createTRPCRouter({ .input(validation.group.update) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.id); + await throwIfGroupNameIsReservedAsync(ctx.db, input.id); - const normalizedName = normalizeName(input.name); - await checkSimilarNameAndThrowAsync(ctx.db, normalizedName, input.id); + await checkSimilarNameAndThrowAsync(ctx.db, input.name, input.id); await ctx.db .update(groups) .set({ - name: normalizedName, + name: input.name, }) .where(eq(groups.id, input.id)); }), @@ -169,6 +169,7 @@ export const groupRouter = createTRPCRouter({ .input(validation.group.groupUser) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); + await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await ctx.db .update(groups) @@ -182,6 +183,7 @@ export const groupRouter = createTRPCRouter({ .input(validation.common.byId) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.id); + await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await ctx.db.delete(groups).where(eq(groups.id, input.id)); }), @@ -190,6 +192,7 @@ export const groupRouter = createTRPCRouter({ .input(validation.group.groupUser) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); + await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); throwIfCredentialsDisabled(); const user = await ctx.db.query.users.findFirst({ @@ -213,6 +216,7 @@ export const groupRouter = createTRPCRouter({ .input(validation.group.groupUser) .mutation(async ({ input, ctx }) => { await throwIfGroupNotFoundAsync(ctx.db, input.groupId); + await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); throwIfCredentialsDisabled(); await ctx.db @@ -221,8 +225,6 @@ export const groupRouter = createTRPCRouter({ }), }); -const normalizeName = (name: string) => name.trim(); - const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreId?: string) => { const similar = await db.query.groups.findFirst({ where: and(like(groups.name, `${name}`), not(eq(groups.id, ignoreId ?? ""))), @@ -236,6 +238,17 @@ const checkSimilarNameAndThrowAsync = async (db: Database, name: string, ignoreI } }; +const throwIfGroupNameIsReservedAsync = async (db: Database, id: string) => { + const count = await db.$count(groups, and(eq(groups.id, id), eq(groups.name, everyoneGroup))); + + if (count > 0) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Action is forbidden for reserved group names", + }); + } +}; + const throwIfGroupNotFoundAsync = async (db: Database, id: string) => { const group = await db.query.groups.findFirst({ where: eq(groups.id, id), diff --git a/packages/auth/events.ts b/packages/auth/events.ts index f4cd1eeac..292204de0 100644 --- a/packages/auth/events.ts +++ b/packages/auth/events.ts @@ -5,6 +5,7 @@ import type { NextAuthConfig } from "next-auth"; import { and, eq, inArray } from "@homarr/db"; import type { Database } from "@homarr/db"; import { groupMembers, groups, users } from "@homarr/db/schema/sqlite"; +import { everyoneGroup } from "@homarr/definitions"; import { logger } from "@homarr/log"; import { env } from "./env.mjs"; @@ -33,6 +34,7 @@ export const createSignInEventHandler = (db: Database): Exclude { + const dbEveryoneGroup = await db.query.groups.findFirst({ + where: eq(groups.name, everyoneGroup), + with: { + members: { + where: eq(groupMembers.userId, userId), + }, + }, + }); + + if (dbEveryoneGroup?.members.length === 0) { + await db.insert(groupMembers).values({ + userId, + groupId: dbEveryoneGroup.id, + }); + logger.info(`Added user to everyone group. user=${userId}`); + } +}; + const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: string, externalGroups: string[]) => { + const ignoredGroups = [everyoneGroup]; const dbGroupMembers = await db.query.groupMembers.findMany({ where: eq(groupMembers.userId, userId), with: { @@ -102,11 +124,11 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s } /** - * The below groups are those groups the user is part of in Homarr, but not in the external system. + * The below groups are those groups the user is part of in Homarr, but not in the external system and not ignored. * So he has to be removed from those groups. */ const groupsUserIsNoLongerMemberOfExternally = dbGroupMembers.filter( - ({ group }) => !externalGroups.includes(group.name), + ({ group }) => !externalGroups.concat(ignoredGroups).includes(group.name), ); if (groupsUserIsNoLongerMemberOfExternally.length > 0) { diff --git a/packages/auth/test/events.spec.ts b/packages/auth/test/events.spec.ts index 02f44abd3..325d1d949 100644 --- a/packages/auth/test/events.spec.ts +++ b/packages/auth/test/events.spec.ts @@ -7,6 +7,7 @@ import { eq } from "@homarr/db"; import type { Database } from "@homarr/db"; import { groupMembers, groups, users } from "@homarr/db/schema/sqlite"; import { createDb } from "@homarr/db/test"; +import { everyoneGroup } from "@homarr/definitions"; import { createSignInEventHandler } from "../events"; @@ -34,6 +35,29 @@ vi.mock("next/headers", async (importOriginal) => { }); describe("createSignInEventHandler should create signInEventHandler", () => { + describe("signInEventHandler should add users to everyone group", () => { + test("should add user to everyone group if he isn't already", async () => { + // Arrange + const db = createDb(); + await createUserAsync(db); + await createGroupAsync(db, everyoneGroup); + const eventHandler = createSignInEventHandler(db); + + // Act + await eventHandler?.({ + user: { id: "1", name: "test" }, + profile: undefined, + account: null, + }); + + // Assert + const dbGroupMembers = await db.query.groupMembers.findFirst({ + where: eq(groupMembers.userId, "1"), + }); + expect(dbGroupMembers?.groupId).toBe("1"); + }); + }); + describe("signInEventHandler should synchronize ldap groups", () => { test("should add missing group membership", async () => { // Arrange @@ -79,6 +103,30 @@ describe("createSignInEventHandler should create signInEventHandler", () => { }); expect(dbGroupMembers).toBeUndefined(); }); + test("should not remove group membership for everyone group", async () => { + // Arrange + const db = createDb(); + await createUserAsync(db); + await createGroupAsync(db, everyoneGroup); + await db.insert(groupMembers).values({ + userId: "1", + groupId: "1", + }); + const eventHandler = createSignInEventHandler(db); + + // Act + await eventHandler?.({ + user: { id: "1", name: "test", groups: [] } as never, + profile: undefined, + account: null, + }); + + // Assert + const dbGroupMembers = await db.query.groupMembers.findFirst({ + where: eq(groupMembers.userId, "1"), + }); + expect(dbGroupMembers?.groupId).toBe("1"); + }); }); describe("signInEventHandler should synchronize oidc groups", () => { test("should add missing group membership", async () => { @@ -125,6 +173,30 @@ describe("createSignInEventHandler should create signInEventHandler", () => { }); expect(dbGroupMembers).toBeUndefined(); }); + test("should not remove group membership for everyone group", async () => { + // Arrange + const db = createDb(); + await createUserAsync(db); + await createGroupAsync(db, everyoneGroup); + await db.insert(groupMembers).values({ + userId: "1", + groupId: "1", + }); + const eventHandler = createSignInEventHandler(db); + + // Act + await eventHandler?.({ + user: { id: "1", name: "test" }, + profile: { preferred_username: "test", someRandomGroupsKey: [] }, + account: null, + }); + + // Assert + const dbGroupMembers = await db.query.groupMembers.findFirst({ + where: eq(groupMembers.userId, "1"), + }); + expect(dbGroupMembers?.groupId).toBe("1"); + }); }); test.each([ ["ldap" as const, { name: "test-new" }, undefined], @@ -183,8 +255,8 @@ const createUserAsync = async (db: Database) => colorScheme: "dark", }); -const createGroupAsync = async (db: Database) => +const createGroupAsync = async (db: Database, name = "test") => await db.insert(groups).values({ id: "1", - name: "test", + name, }); diff --git a/packages/db/migrations/mysql/0013_youthful_vulture.sql b/packages/db/migrations/mysql/0013_youthful_vulture.sql new file mode 100644 index 000000000..0ca6ea7b9 --- /dev/null +++ b/packages/db/migrations/mysql/0013_youthful_vulture.sql @@ -0,0 +1 @@ +ALTER TABLE `group` ADD CONSTRAINT `group_name_unique` UNIQUE(`name`); \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/0013_snapshot.json b/packages/db/migrations/mysql/meta/0013_snapshot.json new file mode 100644 index 000000000..69a7e253f --- /dev/null +++ b/packages/db/migrations/mysql/meta/0013_snapshot.json @@ -0,0 +1,1527 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "aa507e60-8e16-4546-b7a5-86304be877ab", + "prevId": "ae1da11f-d73a-427c-8581-3ea6aabdd6d2", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": ["board_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": ["board_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "groupId": { + "name": "groupId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_groupId_group_id_fk": { + "name": "groupMember_groupId_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_userId_user_id_fk": { + "name": "groupMember_userId_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_groupId_userId_pk": { + "name": "groupMember_groupId_userId_pk", + "columns": ["groupId", "userId"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "groupId": { + "name": "groupId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_groupId_group_id_fk": { + "name": "groupPermission_groupId_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "iconRepository_id": { + "name": "iconRepository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_slug": { + "name": "iconRepository_slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_iconRepository_id": { + "name": "iconRepository_iconRepository_id", + "columns": ["iconRepository_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "icon": { + "name": "icon", + "columns": { + "icon_id": { + "name": "icon_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_name": { + "name": "icon_name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_checksum": { + "name": "icon_checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_id": { + "name": "iconRepository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_iconRepository_id_iconRepository_iconRepository_id_fk": { + "name": "icon_iconRepository_id_iconRepository_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["iconRepository_id"], + "columnsTo": ["iconRepository_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_icon_id": { + "name": "icon_icon_id", + "columns": ["icon_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": ["integration_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": ["item_id", "integration_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": ["integration_id", "kind"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": ["integration_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serverSetting_key": { + "name": "serverSetting_key", + "columns": ["key"] + } + }, + "uniqueConstraints": { + "serverSetting_key_unique": { + "name": "serverSetting_key_unique", + "columns": ["key"] + } + }, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_sessionToken": { + "name": "session_sessionToken", + "columns": ["sessionToken"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "homeBoardId": { + "name": "homeBoardId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colorScheme": { + "name": "colorScheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "firstDayOfWeek": { + "name": "firstDayOfWeek", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "pingIconsEnabled": { + "name": "pingIconsEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_homeBoardId_board_id_fk": { + "name": "user_homeBoardId_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["homeBoardId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index ac2bc7f14..7ac6a43d8 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1729348221072, "tag": "0012_abnormal_wendell_vaughn", "breakpoints": true + }, + { + "idx": 13, + "version": "5", + "when": 1729369383739, + "tag": "0013_youthful_vulture", + "breakpoints": true } ] } diff --git a/packages/db/migrations/mysql/migrate.ts b/packages/db/migrations/mysql/migrate.ts index 50d495d00..7cb343ceb 100644 --- a/packages/db/migrations/mysql/migrate.ts +++ b/packages/db/migrations/mysql/migrate.ts @@ -3,25 +3,35 @@ import { drizzle } from "drizzle-orm/mysql2"; import { migrate } from "drizzle-orm/mysql2/migrator"; import mysql from "mysql2"; +import type { Database } from "../.."; +import * as mysqlSchema from "../../schema/mysql"; +import { seedDataAsync } from "../seed"; + const migrationsFolder = process.argv[2] ?? "."; -const mysql2 = mysql.createConnection( - process.env.DB_HOST - ? { - host: process.env.DB_HOST, - database: process.env.DB_NAME!, - port: Number(process.env.DB_PORT), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - } - : { uri: process.env.DB_URL }, -); +const migrateAsync = async () => { + const mysql2 = mysql.createConnection( + process.env.DB_HOST + ? { + host: process.env.DB_HOST, + database: process.env.DB_NAME!, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + } + : { uri: process.env.DB_URL }, + ); -const db = drizzle(mysql2, { - mode: "default", -}); + const db = drizzle(mysql2, { + mode: "default", + schema: mysqlSchema, + }); -migrate(db, { migrationsFolder }) + await migrate(db, { migrationsFolder }); + await seedDataAsync(db as unknown as Database); +}; + +migrateAsync() .then(() => { console.log("Migration complete"); process.exit(0); diff --git a/packages/db/migrations/run-seed.ts b/packages/db/migrations/run-seed.ts new file mode 100644 index 000000000..7a0e346bc --- /dev/null +++ b/packages/db/migrations/run-seed.ts @@ -0,0 +1,12 @@ +import { database } from "../driver"; +import { seedDataAsync } from "./seed"; + +seedDataAsync(database) + .then(() => { + console.log("Seed complete"); + process.exit(0); + }) + .catch((err) => { + console.log("Seed failed\n\t", err); + process.exit(1); + }); diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts new file mode 100644 index 000000000..ab4a865e7 --- /dev/null +++ b/packages/db/migrations/seed.ts @@ -0,0 +1,52 @@ +import SuperJSON from "superjson"; + +import { everyoneGroup } from "@homarr/definitions"; +import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; + +import { createId, eq } from ".."; +import type { Database } from ".."; +import { groups } from "../schema/mysql"; +import { serverSettings } from "../schema/sqlite"; + +export const seedDataAsync = async (db: Database) => { + await seedEveryoneGroupAsync(db); + await seedServerSettingsAsync(db); +}; + +const seedEveryoneGroupAsync = async (db: Database) => { + const group = await db.query.groups.findFirst({ + where: eq(groups.name, everyoneGroup), + }); + + if (group) { + console.log("Skipping seeding of group 'everyone' as it already exists"); + return; + } + + await db.insert(groups).values({ + id: createId(), + name: everyoneGroup, + }); + console.log("Created group 'everyone' through seed"); +}; + +const seedServerSettingsAsync = async (db: Database) => { + const serverSettingsData = await db.query.serverSettings.findMany(); + let insertedSettingsCount = 0; + + for (const settingsKey of defaultServerSettingsKeys) { + if (serverSettingsData.some((setting) => setting.settingKey === settingsKey)) { + return; + } + + await db.insert(serverSettings).values({ + settingKey: settingsKey, + value: SuperJSON.stringify(defaultServerSettings[settingsKey]), + }); + insertedSettingsCount++; + } + + if (insertedSettingsCount > 0) { + console.info(`Inserted ${insertedSettingsCount} missing settings`); + } +}; diff --git a/packages/db/migrations/sqlite/0013_faithful_hex.sql b/packages/db/migrations/sqlite/0013_faithful_hex.sql new file mode 100644 index 000000000..d2632a0dd --- /dev/null +++ b/packages/db/migrations/sqlite/0013_faithful_hex.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `group_name_unique` ON `group` (`name`); \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0013_snapshot.json b/packages/db/migrations/sqlite/meta/0013_snapshot.json new file mode 100644 index 000000000..1dd77999c --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0013_snapshot.json @@ -0,0 +1,1461 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "767f5db4-59ab-46c7-a154-4fcd9474c358", + "prevId": "20b3ebeb-a111-4576-b530-a2634d154132", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": ["provider", "providerAccountId"], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": ["board_id", "group_id", "permission"], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": ["board_id", "user_id", "permission"], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "groupId": { + "name": "groupId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_groupId_group_id_fk": { + "name": "groupMember_groupId_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_userId_user_id_fk": { + "name": "groupMember_userId_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_groupId_userId_pk": { + "columns": ["groupId", "userId"], + "name": "groupMember_groupId_userId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "groupId": { + "name": "groupId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_groupId_group_id_fk": { + "name": "groupPermission_groupId_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "iconRepository_id": { + "name": "iconRepository_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "iconRepository_slug": { + "name": "iconRepository_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "icon_id": { + "name": "icon_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_name": { + "name": "icon_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_checksum": { + "name": "icon_checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "iconRepository_id": { + "name": "iconRepository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_iconRepository_id_iconRepository_iconRepository_id_fk": { + "name": "icon_iconRepository_id_iconRepository_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["iconRepository_id"], + "columnsTo": ["iconRepository_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": ["integration_id", "group_id", "permission"], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": ["item_id", "integration_id"], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": ["integration_id", "kind"], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": ["integration_id", "user_id", "permission"], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_key_unique": { + "name": "serverSetting_key_unique", + "columns": ["key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "homeBoardId": { + "name": "homeBoardId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colorScheme": { + "name": "colorScheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "firstDayOfWeek": { + "name": "firstDayOfWeek", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "pingIconsEnabled": { + "name": "pingIconsEnabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_homeBoardId_board_id_fk": { + "name": "user_homeBoardId_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["homeBoardId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": ["identifier", "token"], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index 327142451..e88a243de 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1729348200091, "tag": "0012_ambiguous_black_panther", "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1729369389386, + "tag": "0013_faithful_hex", + "breakpoints": true } ] } diff --git a/packages/db/migrations/sqlite/migrate.ts b/packages/db/migrations/sqlite/migrate.ts index abef5ea9d..47a901745 100644 --- a/packages/db/migrations/sqlite/migrate.ts +++ b/packages/db/migrations/sqlite/migrate.ts @@ -2,10 +2,27 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { schema } from "../.."; +import { seedDataAsync } from "../seed"; + const migrationsFolder = process.argv[2] ?? "."; -const sqlite = new Database(process.env.DB_URL?.replace("file:", "")); +const migrateAsync = async () => { + const sqlite = new Database(process.env.DB_URL?.replace("file:", "")); -const db = drizzle(sqlite); + const db = drizzle(sqlite, { schema }); -migrate(db, { migrationsFolder }); + migrate(db, { migrationsFolder }); + + await seedDataAsync(db); +}; + +migrateAsync() + .then(() => { + console.log("Migration complete"); + process.exit(0); + }) + .catch((err) => { + console.log("Migration failed", err); + process.exit(1); + }); diff --git a/packages/db/package.json b/packages/db/package.json index db156f545..9ef951981 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -21,15 +21,17 @@ "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint", "migration:mysql:generate": "drizzle-kit generate --config ./configs/mysql.config.ts", - "migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts", + "migration:mysql:run": "drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed", "migration:mysql:drop": "drizzle-kit drop --config ./configs/mysql.config.ts", "migration:sqlite:generate": "drizzle-kit generate --config ./configs/sqlite.config.ts", - "migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts", + "migration:sqlite:run": "drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed", "migration:sqlite:drop": "drizzle-kit drop --config ./configs/sqlite.config.ts", "push:mysql": "drizzle-kit push --config ./configs/mysql.config.ts", "push:sqlite": "drizzle-kit push --config ./configs/sqlite.config.ts", + "seed": "pnpm with-env tsx ./migrations/run-seed.ts", "studio": "drizzle-kit studio --config ./configs/sqlite.config.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "with-env": "dotenv -e ../../.env --" }, "prettier": "@homarr/prettier-config", "dependencies": { @@ -37,6 +39,7 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", + "@homarr/server-settings": "workspace:^0.1.0", "@paralleldrive/cuid2": "^2.2.2", "@testcontainers/mysql": "^10.13.2", "better-sqlite3": "^11.4.0", @@ -53,6 +56,7 @@ "dotenv-cli": "^7.4.2", "eslint": "^9.13.0", "prettier": "^3.3.3", + "tsx": "4.19.1", "typescript": "^5.6.3" } } diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 510ddaf89..e5b2b050f 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -120,7 +120,7 @@ export const groupMembers = mysqlTable( export const groups = mysqlTable("group", { id: varchar("id", { length: 64 }).notNull().primaryKey(), - name: varchar("name", { length: 64 }).notNull(), + name: varchar("name", { length: 64 }).unique().notNull(), ownerId: varchar("owner_id", { length: 64 }).references(() => users.id, { onDelete: "set null", }), diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index f0d1a52e4..08a8b8c44 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -121,7 +121,7 @@ export const groupMembers = sqliteTable( export const groups = sqliteTable("group", { id: text("id").notNull().primaryKey(), - name: text("name").notNull(), + name: text("name").unique().notNull(), ownerId: text("owner_id").references(() => users.id, { onDelete: "set null", }), diff --git a/packages/definitions/package.json b/packages/definitions/package.json index 4beccf3e0..e58e30125 100644 --- a/packages/definitions/package.json +++ b/packages/definitions/package.json @@ -18,7 +18,8 @@ "clean": "rm -rf .turbo node_modules", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "postinstall": "tsx ./src/docs/codegen.ts" }, "prettier": "@homarr/prettier-config", "dependencies": { diff --git a/packages/definitions/src/docs/codegen.ts b/packages/definitions/src/docs/codegen.ts new file mode 100644 index 000000000..9f35a3af3 --- /dev/null +++ b/packages/definitions/src/docs/codegen.ts @@ -0,0 +1,75 @@ +import fs from "fs/promises"; +import path, { dirname } from "path"; +import { fileURLToPath } from "url"; +import { XMLParser } from "fast-xml-parser"; +import { z } from "zod"; + +import { createDocumentationLink } from "./index"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const removeCommonUrl = (url: string) => { + return url.replace("https://homarr.dev", ""); +}; + +const sitemapSchema = z.object({ + urlset: z.object({ + url: z.array( + z.object({ + loc: z.string(), + }), + ), + }), +}); + +const fetchSitemapAsync = async () => { + const response = await fetch(createDocumentationLink("/sitemap.xml")); + return await response.text(); +}; + +const parseXml = (sitemapXml: string) => { + const parser = new XMLParser(); + const data: unknown = parser.parse(sitemapXml); + const result = sitemapSchema.safeParse(data); + if (!result.success) { + throw new Error("Invalid sitemap schema"); + } + + return result.data; +}; + +const mapSitemapXmlToPaths = (sitemapData: z.infer) => { + return sitemapData.urlset.url.map((url) => removeCommonUrl(url.loc)); +}; + +const createSitemapPathType = (paths: string[]) => { + return "export type HomarrDocumentationPath =\n" + paths.map((path) => ` | "${path.replace(/\/$/, "")}"`).join("\n"); +}; + +const updateSitemapTypeFileAsync = async (sitemapPathType: string) => { + const content = + "// This file is auto-generated by the codegen script\n" + + "// it uses the sitemap.xml to generate the HomarrDocumentationPath type\n" + + sitemapPathType + + ";\n"; + + await fs.writeFile(path.join(__dirname, "homarr-docs-sitemap.ts"), content); +}; + +/** + * This script fetches the sitemap.xml and generates the HomarrDocumentationPath type + * which is used for typesafe documentation links + */ +// eslint-disable-next-line no-restricted-syntax +const main = async () => { + const sitemapXml = await fetchSitemapAsync(); + const sitemapData = parseXml(sitemapXml); + const paths = mapSitemapXmlToPaths(sitemapData); + // Adding sitemap as it's not in the sitemap.xml and we need it for this file + paths.push("/sitemap.xml"); + const sitemapPathType = createSitemapPathType(paths); + await updateSitemapTypeFileAsync(sitemapPathType); +}; + +void main(); diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts new file mode 100644 index 000000000..0e4ffbd96 --- /dev/null +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -0,0 +1,191 @@ +// This file is auto-generated by the codegen script +// it uses the sitemap.xml to generate the HomarrDocumentationPath type +export type HomarrDocumentationPath = + | "/about-us" + | "/blog" + | "/blog/2023/01/11/version0.11" + | "/blog/2023/04/16/version0.12-more-widgets" + | "/blog/2023/11/10/authentication" + | "/blog/2023/12/22/updated-documentation" + | "/blog/2024/09/23/version-1.0" + | "/blog/archive" + | "/blog/authors" + | "/blog/authors/ajnart" + | "/blog/authors/manuel-rw" + | "/blog/authors/meierschlumpf" + | "/blog/authors/tagashi" + | "/blog/authors/walkx" + | "/blog/documentation-migration" + | "/blog/tags" + | "/blog/tags/authentication" + | "/blog/tags/breaking-changes" + | "/blog/tags/contributions" + | "/blog/tags/design" + | "/blog/tags/dnd" + | "/blog/tags/docs" + | "/blog/tags/documentation" + | "/blog/tags/gridstack" + | "/blog/tags/homarr" + | "/blog/tags/migration" + | "/blog/tags/notepad" + | "/blog/tags/security" + | "/blog/tags/translations" + | "/blog/tags/update" + | "/blog/tags/version" + | "/blog/translations" + | "/docs/tags" + | "/docs/tags/active-directory" + | "/docs/tags/ad-guard" + | "/docs/tags/ad-guard-home" + | "/docs/tags/administration" + | "/docs/tags/advanced" + | "/docs/tags/analytics" + | "/docs/tags/api" + | "/docs/tags/banner" + | "/docs/tags/blocking" + | "/docs/tags/board" + | "/docs/tags/boards" + | "/docs/tags/bookmark" + | "/docs/tags/caddy" + | "/docs/tags/checklist" + | "/docs/tags/code" + | "/docs/tags/community" + | "/docs/tags/configuration" + | "/docs/tags/connections" + | "/docs/tags/customization" + | "/docs/tags/data-sources" + | "/docs/tags/developer" + | "/docs/tags/development" + | "/docs/tags/dns" + | "/docs/tags/docker" + | "/docs/tags/edit-mode" + | "/docs/tags/env" + | "/docs/tags/environment-variables" + | "/docs/tags/feeds" + | "/docs/tags/getting-started" + | "/docs/tags/google" + | "/docs/tags/grafana" + | "/docs/tags/groups" + | "/docs/tags/hardware" + | "/docs/tags/health" + | "/docs/tags/help" + | "/docs/tags/icons" + | "/docs/tags/iframe" + | "/docs/tags/images" + | "/docs/tags/installation" + | "/docs/tags/integrade" + | "/docs/tags/integration" + | "/docs/tags/integrations" + | "/docs/tags/interface" + | "/docs/tags/jellyserr" + | "/docs/tags/ldap" + | "/docs/tags/links" + | "/docs/tags/lists" + | "/docs/tags/management" + | "/docs/tags/monitoring" + | "/docs/tags/news" + | "/docs/tags/notebook" + | "/docs/tags/notes" + | "/docs/tags/oidc" + | "/docs/tags/open-media-vault" + | "/docs/tags/overseerr" + | "/docs/tags/permissions" + | "/docs/tags/pi-hole" + | "/docs/tags/preferences" + | "/docs/tags/programming" + | "/docs/tags/proxmox" + | "/docs/tags/proxy" + | "/docs/tags/roles" + | "/docs/tags/rss" + | "/docs/tags/search" + | "/docs/tags/search-engines" + | "/docs/tags/security" + | "/docs/tags/seo" + | "/docs/tags/server" + | "/docs/tags/settings" + | "/docs/tags/sinkhole" + | "/docs/tags/sso" + | "/docs/tags/system" + | "/docs/tags/table" + | "/docs/tags/technical-documentation" + | "/docs/tags/text" + | "/docs/tags/theming" + | "/docs/tags/traefik" + | "/docs/tags/translations" + | "/docs/tags/unraid" + | "/docs/tags/user" + | "/docs/tags/users" + | "/docs/tags/variables" + | "/docs/tags/widgets" + | "/docs/advanced/command-line" + | "/docs/advanced/command-line/password-recovery" + | "/docs/advanced/configuration/environment-variables" + | "/docs/advanced/configuration/keyboard-shortcuts" + | "/docs/advanced/configuration/proxies-and-certificates" + | "/docs/advanced/customizations/board-customization" + | "/docs/advanced/customizations/dark-mode" + | "/docs/advanced/customizations/icons" + | "/docs/advanced/customizations/user-preferences" + | "/docs/advanced/sso" + | "/docs/category/advanced" + | "/docs/category/getting-started" + | "/docs/category/installation" + | "/docs/category/installation-1" + | "/docs/category/integrations" + | "/docs/category/management" + | "/docs/category/more" + | "/docs/category/widgets" + | "/docs/community/developer-guides" + | "/docs/community/donate" + | "/docs/community/faq" + | "/docs/community/get-in-touch" + | "/docs/community/license" + | "/docs/community/translations" + | "/docs/getting-started" + | "/docs/getting-started/after-the-installation" + | "/docs/getting-started/glossary" + | "/docs/getting-started/installation/docker" + | "/docs/getting-started/installation/easy-panel" + | "/docs/getting-started/installation/home-assistant" + | "/docs/getting-started/installation/kubernetes" + | "/docs/getting-started/installation/portainer" + | "/docs/getting-started/installation/qnap" + | "/docs/getting-started/installation/saltbox" + | "/docs/getting-started/installation/source" + | "/docs/getting-started/installation/synology" + | "/docs/getting-started/installation/truenas" + | "/docs/getting-started/installation/unraid" + | "/docs/integrations/containers" + | "/docs/integrations/dns" + | "/docs/integrations/hardware" + | "/docs/integrations/media-requester" + | "/docs/integrations/media-server" + | "/docs/integrations/servarr" + | "/docs/integrations/torrent" + | "/docs/integrations/usenet" + | "/docs/management/api" + | "/docs/management/boards" + | "/docs/management/integrations" + | "/docs/management/search-engines" + | "/docs/management/settings" + | "/docs/management/users" + | "/docs/widgets/bookmarks" + | "/docs/widgets/calendar-widget" + | "/docs/widgets/clock-widget" + | "/docs/widgets/dashdot-widget" + | "/docs/widgets/dns-hole" + | "/docs/widgets/download-speed-widget" + | "/docs/widgets/health-monitoring" + | "/docs/widgets/home-assistant" + | "/docs/widgets/iframe" + | "/docs/widgets/indexer-manager" + | "/docs/widgets/media-requests" + | "/docs/widgets/media-server" + | "/docs/widgets/notebook" + | "/docs/widgets/rss-widget" + | "/docs/widgets/torrent-widget" + | "/docs/widgets/usenet-widget" + | "/docs/widgets/video" + | "/docs/widgets/weather-widget" + | "" + | "/sitemap.xml"; diff --git a/packages/definitions/src/docs/index.ts b/packages/definitions/src/docs/index.ts new file mode 100644 index 000000000..cc6c0f515 --- /dev/null +++ b/packages/definitions/src/docs/index.ts @@ -0,0 +1,7 @@ +import type { HomarrDocumentationPath } from "./homarr-docs-sitemap"; + +const documentationBaseUrl = "https://deploy-preview-113--homarr-docs.netlify.app"; + +// Please use the method so the path can be checked! +export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) => + `${documentationBaseUrl}${path}${hashTag ?? ""}`; diff --git a/packages/definitions/src/group.ts b/packages/definitions/src/group.ts new file mode 100644 index 000000000..ebac0daa1 --- /dev/null +++ b/packages/definitions/src/group.ts @@ -0,0 +1 @@ +export const everyoneGroup = "everyone"; diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index d2dcd8e0a..20919b208 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -6,3 +6,5 @@ export * from "./permissions"; export * from "./docker"; export * from "./auth"; export * from "./user"; +export * from "./group"; +export * from "./docs"; diff --git a/packages/spotlight/src/modes/index.tsx b/packages/spotlight/src/modes/index.tsx index afb613e74..eb286a1cd 100644 --- a/packages/spotlight/src/modes/index.tsx +++ b/packages/spotlight/src/modes/index.tsx @@ -1,6 +1,7 @@ import { Group, Kbd, Text } from "@mantine/core"; import { IconBook2, IconBrandDiscord, IconBrandGithub } from "@tabler/icons-react"; +import { createDocumentationLink } from "@homarr/definitions"; import { useScopedI18n } from "@homarr/translation/client"; import { createGroup } from "../lib/group"; @@ -45,7 +46,7 @@ const helpMode = { { label: t("documentation.label"), icon: IconBook2, - href: "https://homarr.dev/docs/getting-started/", + href: createDocumentationLink("/docs/getting-started"), }, { label: t("submitIssue.label"), diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 834326730..670be6712 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -249,6 +249,9 @@ export default { mixed: "Some members are from external providers and cannot be managed here", external: "All members are from external providers and cannot be managed here", }, + reservedNotice: { + message: "This group is reserved for system use and restricts some actions. {checkoutDocs}", + }, action: { create: { label: "New group", diff --git a/packages/validation/src/group.ts b/packages/validation/src/group.ts index cd476c5df..a48642caf 100644 --- a/packages/validation/src/group.ts +++ b/packages/validation/src/group.ts @@ -1,12 +1,19 @@ import { z } from "zod"; -import { groupPermissionKeys } from "@homarr/definitions"; +import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions"; import { byIdSchema } from "./common"; import { zodEnumFromArray } from "./enums"; const createSchema = z.object({ - name: z.string().trim().min(1).max(64), + name: z + .string() + .trim() + .min(1) + .max(64) + .refine((value) => value !== everyoneGroup, { + message: "'everyone' is a reserved group name", + }), }); const updateSchema = createSchema.merge(byIdSchema); diff --git a/packages/widgets/src/video/component.tsx b/packages/widgets/src/video/component.tsx index 0893e6d8d..3527246db 100644 --- a/packages/widgets/src/video/component.tsx +++ b/packages/widgets/src/video/component.tsx @@ -13,6 +13,8 @@ import classes from "./component.module.css"; import "video.js/dist/video-js.css"; +import { createDocumentationLink } from "@homarr/definitions"; + export default function VideoWidget({ options }: WidgetComponentProps<"video">) { if (options.feedUrl.trim() === "") { return ; @@ -46,7 +48,7 @@ const ForYoutubeUseIframe = () => { {t("widget.video.error.forYoutubeUseIframe")} - {t("common.action.checkoutDocs")} + {t("common.action.checkoutDocs")} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9081ca2bb..db7fe9746 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 4.3.3(vite@5.4.5(@types/node@20.16.13)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/coverage-v8': specifier: ^2.1.3 - version: 2.1.3(vitest@2.1.3) + version: 2.1.3(vitest@2.1.3(@types/node@20.16.13)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/ui': specifier: ^2.1.3 version: 2.1.3(vitest@2.1.3) @@ -358,8 +358,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 tsx: - specifier: 4.13.3 - version: 4.13.3 + specifier: 4.19.1 + version: 4.19.1 typescript: specifier: ^5.6.3 version: 5.6.3 @@ -394,8 +394,8 @@ importers: specifier: ^16.4.5 version: 16.4.5 tsx: - specifier: 4.13.3 - version: 4.13.3 + specifier: 4.19.1 + version: 4.19.1 ws: specifier: ^8.18.0 version: 8.18.0 @@ -844,6 +844,9 @@ importers: '@homarr/log': specifier: workspace:^0.1.0 version: link:../log + '@homarr/server-settings': + specifier: workspace:^0.1.0 + version: link:../server-settings '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 @@ -887,6 +890,9 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 + tsx: + specifier: 4.19.1 + version: 4.19.1 typescript: specifier: ^5.6.3 version: 5.6.3 @@ -1907,6 +1913,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1931,6 +1943,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -1955,6 +1973,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -1979,6 +2003,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -2003,6 +2033,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -2027,6 +2063,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -2051,6 +2093,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -2075,6 +2123,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -2099,6 +2153,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -2123,6 +2183,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -2147,6 +2213,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2171,6 +2243,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2195,6 +2273,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2219,6 +2303,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2243,6 +2333,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -2267,6 +2363,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -2291,6 +2393,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -2315,6 +2423,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -2339,6 +2459,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2363,6 +2489,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2387,6 +2519,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -2411,6 +2549,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -2435,6 +2579,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4707,6 +4857,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7339,8 +7494,8 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} - tsx@4.13.3: - resolution: {integrity: sha512-FTAJJLQCMiIbt78kD5qhLjHIR5NOQDKC63wcdelWRDBE+d1xSrXYhXq4DzejnC2tGhFZHpDy2Ika0Ugf7sK8gA==} + tsx@4.19.1: + resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} hasBin: true @@ -8180,6 +8335,9 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true @@ -8192,6 +8350,9 @@ snapshots: '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -8204,6 +8365,9 @@ snapshots: '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-x64@0.18.20': optional: true @@ -8216,6 +8380,9 @@ snapshots: '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true @@ -8228,6 +8395,9 @@ snapshots: '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true @@ -8240,6 +8410,9 @@ snapshots: '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true @@ -8252,6 +8425,9 @@ snapshots: '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true @@ -8264,6 +8440,9 @@ snapshots: '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true @@ -8276,6 +8455,9 @@ snapshots: '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true @@ -8288,6 +8470,9 @@ snapshots: '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true @@ -8300,6 +8485,9 @@ snapshots: '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -8312,6 +8500,9 @@ snapshots: '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true @@ -8324,6 +8515,9 @@ snapshots: '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true @@ -8336,6 +8530,9 @@ snapshots: '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true @@ -8348,6 +8545,9 @@ snapshots: '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true @@ -8360,6 +8560,9 @@ snapshots: '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true @@ -8372,6 +8575,9 @@ snapshots: '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true @@ -8384,6 +8590,12 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true @@ -8396,6 +8608,9 @@ snapshots: '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true @@ -8408,6 +8623,9 @@ snapshots: '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true @@ -8420,6 +8638,9 @@ snapshots: '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true @@ -8432,6 +8653,9 @@ snapshots: '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true @@ -8444,6 +8668,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@9.13.0)': dependencies: eslint: 9.13.0 @@ -9930,7 +10157,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.3(vitest@2.1.3)': + '@vitest/coverage-v8@2.1.3(vitest@2.1.3(@types/node@20.16.13)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -11214,6 +11441,33 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-goat@2.1.1: {} @@ -14149,9 +14403,9 @@ snapshots: tsscmp@1.0.6: {} - tsx@4.13.3: + tsx@4.19.1: dependencies: - esbuild: 0.20.2 + esbuild: 0.23.1 get-tsconfig: 4.8.1 optionalDependencies: fsevents: 2.3.3 diff --git a/tooling/eslint/base.js b/tooling/eslint/base.js index 118762731..47632dbc2 100644 --- a/tooling/eslint/base.js +++ b/tooling/eslint/base.js @@ -39,7 +39,7 @@ export default tseslint.config( "warn", { min: 3, - exceptions: ["_", "i", "z", "t", "id", "db"], // _ for unused variables, i for index, z for zod, t for translation + exceptions: ["_", "i", "z", "t", "id", "db", "fs"], // _ for unused variables, i for index, z for zod, t for translation properties: "never", // This allows for example the use of as sm and md would be too short }, ], @@ -80,6 +80,11 @@ export default tseslint.config( "VariableDeclarator[init.type=/FunctionExpression$/][init.async=true][id.name=/^[a-z].*$/][id.name!=/Async$/]", message: "Async function name must end in 'Async' (variable declarator)", }, + { + // \\u002F is the unicode escape for / and is used because of https://github.com/estools/esquery/issues/68 + selector: "Literal[value=/^https:\\u002F\\u002Fhomarr\\.dev\\u002F.*$/]", + message: "Links to 'https://homarr.dev/' should be used with createDocumentationLink method", + }, ], }, },