Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4cdf7aca6 | ||
|
|
75454e7913 | ||
|
|
8a7b1d61b2 | ||
|
|
df5704019b | ||
|
|
5ccb1e70f9 | ||
|
|
82a5fd0118 | ||
|
|
b0a1c5486c | ||
|
|
56aae88853 | ||
|
|
5332f0d5c0 | ||
|
|
bd7a9fbd10 | ||
|
|
7ea71a57b7 |
No files matched your search
@@ -20,7 +20,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Prebuilt debian dependencies
|
- name: Prebuilt debian dependencies
|
||||||
uses: homarr-labs/homarr/.github/actions/prebuilt-debian@dev
|
uses: ./.github/actions/prebuilt-debian@dev
|
||||||
id: prebuilt-debian
|
id: prebuilt-debian
|
||||||
with:
|
with:
|
||||||
architecture: ${{ inputs.architecture }}
|
architecture: ${{ inputs.architecture }}
|
||||||
|
|||||||
@@ -1,44 +1,3 @@
|
|||||||
## [1.47.0](https://github.com/homarr-labs/homarr/compare/v1.46.0...v1.47.0) (2025-12-19)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* [#4452](https://github.com/homarr-labs/homarr/issues/4452) improve calendar design ([#4461](https://github.com/homarr-labs/homarr/issues/4461)) ([08e9f7d](https://github.com/homarr-labs/homarr/commit/08e9f7d8d85475788534b46e6cceb40a7231c09d))
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **boards:** selection menu not shown to users without change access ([#4689](https://github.com/homarr-labs/homarr/issues/4689)) ([a0a11e3](https://github.com/homarr-labs/homarr/commit/a0a11e35707513fee45551e09c26ff633f636c32))
|
|
||||||
* **deps:** update dependency dns-caching to ^0.2.9 ([#4648](https://github.com/homarr-labs/homarr/issues/4648)) ([6deab2e](https://github.com/homarr-labs/homarr/commit/6deab2ecec2dc4f5192b53213cb755a9853eb7c0))
|
|
||||||
* **deps:** update dependency drizzle-orm to ^0.45.1 ([#4663](https://github.com/homarr-labs/homarr/issues/4663)) ([f635009](https://github.com/homarr-labs/homarr/commit/f63500961f11589a972a41dde9e4117b1f25141c))
|
|
||||||
* **deps:** update dependency fast-xml-parser to ^5.3.3 ([#4683](https://github.com/homarr-labs/homarr/issues/4683)) ([611b65e](https://github.com/homarr-labs/homarr/commit/611b65e8bd420aaad580bf35071b651093623c95))
|
|
||||||
* **deps:** update dependency isomorphic-dompurify to ^2.34.0 ([#4650](https://github.com/homarr-labs/homarr/issues/4650)) ([5d5c18b](https://github.com/homarr-labs/homarr/commit/5d5c18b3649d5fa1b2b933d443ccd5abcbb84e3a))
|
|
||||||
* **deps:** update dependency jotai to ^2.16.0 ([#4655](https://github.com/homarr-labs/homarr/issues/4655)) ([4aef85b](https://github.com/homarr-labs/homarr/commit/4aef85ba60f4978da5a0b13c08e1799949fe4497))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.16 ([#4640](https://github.com/homarr-labs/homarr/issues/4640)) ([ada5597](https://github.com/homarr-labs/homarr/commit/ada5597cf3ab0bafb04e9176b431d257b36d1dc8))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.17 ([#4642](https://github.com/homarr-labs/homarr/issues/4642)) ([c09da85](https://github.com/homarr-labs/homarr/commit/c09da85f3a8450a70991ff0656228a9aa1364f89))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.18 ([#4653](https://github.com/homarr-labs/homarr/issues/4653)) ([4bd142e](https://github.com/homarr-labs/homarr/commit/4bd142e09c666cc84c67c93e2202e571565b766c))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.19 ([#4669](https://github.com/homarr-labs/homarr/issues/4669)) ([8ed771d](https://github.com/homarr-labs/homarr/commit/8ed771d30073809b08a6ace2c29b3de2675e37d1))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.20 ([#4684](https://github.com/homarr-labs/homarr/issues/4684)) ([703c8cc](https://github.com/homarr-labs/homarr/commit/703c8ccc060bb1775a72f405da1e169926b1ce5d))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.21 ([#4695](https://github.com/homarr-labs/homarr/issues/4695)) ([7f6eaa0](https://github.com/homarr-labs/homarr/commit/7f6eaa0b1f397a37b6acd15e0ccfea75ee2e0161))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.22 ([#4700](https://github.com/homarr-labs/homarr/issues/4700)) ([391d725](https://github.com/homarr-labs/homarr/commit/391d725191563ba5048593c14baf077a924cea44))
|
|
||||||
* **deps:** update dependency ldapts to v8.0.23 ([#4701](https://github.com/homarr-labs/homarr/issues/4701)) ([db337b2](https://github.com/homarr-labs/homarr/commit/db337b2525762c96acd815bd4fdacfaabff5f0cf))
|
|
||||||
* **deps:** update dependency next-intl to v4.6.0 ([#4691](https://github.com/homarr-labs/homarr/issues/4691)) ([1ff6fac](https://github.com/homarr-labs/homarr/commit/1ff6fac71bb9187d0eb703a7b6543daf8861efe8))
|
|
||||||
* **deps:** update dependency sass to ^1.94.3 ([#4646](https://github.com/homarr-labs/homarr/issues/4646)) ([7e91d12](https://github.com/homarr-labs/homarr/commit/7e91d12439826eee76f118448c583294c3c46b42))
|
|
||||||
* **deps:** update dependency sass to ^1.95.0 ([#4647](https://github.com/homarr-labs/homarr/issues/4647)) ([eb760da](https://github.com/homarr-labs/homarr/commit/eb760da8b30583575fb638995fc2f2acbd5b2961))
|
|
||||||
* **deps:** update dependency sass to ^1.95.1 ([#4661](https://github.com/homarr-labs/homarr/issues/4661)) ([e327c66](https://github.com/homarr-labs/homarr/commit/e327c660b239840c4929f536eff3096342cc3433))
|
|
||||||
* **deps:** update dependency sass to ^1.96.0 ([#4668](https://github.com/homarr-labs/homarr/issues/4668)) ([bdfc150](https://github.com/homarr-labs/homarr/commit/bdfc1501152450a1546962331037a6fc1c782e4e))
|
|
||||||
* **deps:** update dependency swagger-ui-react to ^5.31.0 ([#4672](https://github.com/homarr-labs/homarr/issues/4672)) ([4d593f5](https://github.com/homarr-labs/homarr/commit/4d593f5b9d1f47ae2368f42131e8ef8d32f73fc9))
|
|
||||||
* **deps:** update dependency typescript-eslint to ^8.49.0 ([#4645](https://github.com/homarr-labs/homarr/issues/4645)) ([220cb66](https://github.com/homarr-labs/homarr/commit/220cb669a13fead953ff42040b39e06d1dcc1fe2))
|
|
||||||
* **deps:** update mantine monorepo to ^8.3.10 ([#4651](https://github.com/homarr-labs/homarr/issues/4651)) ([d0652f7](https://github.com/homarr-labs/homarr/commit/d0652f7cf5ac0e63a61adcfe43e53c4ed130e8fc))
|
|
||||||
* **deps:** update react monorepo to v19.2.2 ([#4676](https://github.com/homarr-labs/homarr/issues/4676)) ([82e79f2](https://github.com/homarr-labs/homarr/commit/82e79f26274a289e1ce4f9331b6ba16f00f5a86c))
|
|
||||||
* **deps:** update react monorepo to v19.2.3 ([#4680](https://github.com/homarr-labs/homarr/issues/4680)) ([4c71856](https://github.com/homarr-labs/homarr/commit/4c7185646d9d061d1aebf9194f9ef2f810fc3384))
|
|
||||||
* **deps:** update testcontainers-node monorepo to ^11.10.0 ([#4659](https://github.com/homarr-labs/homarr/issues/4659)) ([c880ce9](https://github.com/homarr-labs/homarr/commit/c880ce9bd99a7056f20d450275fcbc37f9372f63))
|
|
||||||
* **deps:** update trpc monorepo to ^11.8.0 ([#4697](https://github.com/homarr-labs/homarr/issues/4697)) ([1f44845](https://github.com/homarr-labs/homarr/commit/1f44845df489bb33bcfe943c6a05c0f157037ed6))
|
|
||||||
* **downloads:** improve progress to only show 100 percent if fully completed ([#4621](https://github.com/homarr-labs/homarr/issues/4621)) ([1062745](https://github.com/homarr-labs/homarr/commit/10627453d5b5a17991bbe9b8d4bc23981b366c39))
|
|
||||||
* **iframe-widget:** permission selection not applying to any origin ([#4616](https://github.com/homarr-labs/homarr/issues/4616)) ([479f083](https://github.com/homarr-labs/homarr/commit/479f083ab8ec405d1b8296b32a20262f6192a1f9))
|
|
||||||
* **invites:** creation with custom expiration not working ([#4677](https://github.com/homarr-labs/homarr/issues/4677)) ([423da9e](https://github.com/homarr-labs/homarr/commit/423da9eeb88510a123bdd2447dc97b54c5f1dec4))
|
|
||||||
* reference to prebuilt-debian action does not work ([47d7458](https://github.com/homarr-labs/homarr/commit/47d7458143d8801947cae1985745fa827b247dd1))
|
|
||||||
* **stock-price:** use previous-close for change calculation ([#4674](https://github.com/homarr-labs/homarr/issues/4674)) ([17fc7a0](https://github.com/homarr-labs/homarr/commit/17fc7a043b4adbed9f14408e23c95ec5fba0c8b8))
|
|
||||||
* **tdarr:** array parse error ([#4678](https://github.com/homarr-labs/homarr/issues/4678)) ([d86af07](https://github.com/homarr-labs/homarr/commit/d86af072bf136d76b02f3efa2c15520a4f63974e))
|
|
||||||
|
|
||||||
## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12)
|
## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ COPY --from=builder /app/apps/nextjs/package.json .
|
|||||||
|
|
||||||
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||||
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||||
|
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||||
|
|
||||||
COPY --from=builder /app/packages/db/migrations ./db/migrations
|
COPY --from=builder /app/packages/db/migrations ./db/migrations
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"@mantine/modals": "^8.3.10",
|
"@mantine/modals": "^8.3.10",
|
||||||
"@mantine/tiptap": "^8.3.10",
|
"@mantine/tiptap": "^8.3.10",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
|
"@scalar/api-reference-react": "^0.8.11",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@tanstack/react-query-devtools": "^5.91.1",
|
"@tanstack/react-query-devtools": "^5.91.1",
|
||||||
@@ -85,7 +86,6 @@
|
|||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.96.0",
|
"sass": "^1.96.0",
|
||||||
"superjson": "2.2.6",
|
"superjson": "2.2.6",
|
||||||
"swagger-ui-react": "^5.31.0",
|
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,6 @@
|
|||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { OpenAPIObject } from "openapi3-ts/oas31";
|
|
||||||
import SwaggerUI from "swagger-ui-react";
|
|
||||||
|
|
||||||
// workaround for CSS that cannot be processed by next.js, https://github.com/swagger-api/swagger-ui/issues/10045
|
|
||||||
import "../swagger-ui-dark.css";
|
|
||||||
import "../swagger-ui-overrides.css";
|
|
||||||
import "../swagger-ui.css";
|
|
||||||
|
|
||||||
interface SwaggerUIClientProps {
|
|
||||||
document: OpenAPIObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SwaggerUIClient = ({ document }: SwaggerUIClientProps) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const requestInterceptor = (req: Record<string, any>) => {
|
|
||||||
req.credentials = "omit";
|
|
||||||
return req;
|
|
||||||
};
|
|
||||||
|
|
||||||
return <SwaggerUI requestInterceptor={requestInterceptor} spec={document} />;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
|
||||||
|
import { ApiReferenceReact } from "@scalar/api-reference-react";
|
||||||
|
|
||||||
import { openApiDocument } from "@homarr/api";
|
import { openApiDocument } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -8,11 +9,14 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import { SwaggerUIClient } from "~/app/[locale]/manage/tools/api/components/swagger-ui";
|
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { ApiKeysManagement } from "./components/api-keys";
|
import { ApiKeysManagement } from "./components/api-keys";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import "@scalar/api-reference-react/style.css";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||||
return {};
|
return {};
|
||||||
@@ -45,7 +49,21 @@ export default async function ApiPage() {
|
|||||||
<ApiKeysManagement apiKeys={apiKeys} />
|
<ApiKeysManagement apiKeys={apiKeys} />
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
<TabsPanel value={"documentation"}>
|
<TabsPanel value={"documentation"}>
|
||||||
<SwaggerUIClient document={document} />
|
<ApiReferenceReact
|
||||||
|
configuration={{
|
||||||
|
content: document,
|
||||||
|
layout: "classic",
|
||||||
|
showSidebar: false,
|
||||||
|
hideDarkModeToggle: true,
|
||||||
|
showDeveloperTools: "never",
|
||||||
|
hiddenClients: true,
|
||||||
|
darkMode: session.user.colorScheme === "dark",
|
||||||
|
authentication: {
|
||||||
|
preferredSecurityScheme: "apikey",
|
||||||
|
},
|
||||||
|
hideSearch: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +0,0 @@
|
|||||||
.swagger-ui .info {
|
|
||||||
margin: 0 !important;
|
|
||||||
margin-bottom: 20px !important;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.47.0",
|
"version": "1.46.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@tree-sitter-grammars/tree-sitter-yaml",
|
"@tree-sitter-grammars/tree-sitter-yaml",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
"better-sqlite3",
|
||||||
"cpu-features",
|
"cpu-features",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"sharp",
|
"sharp",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { importRouter } from "./router/import/import-router";
|
|||||||
import { infoRouter } from "./router/info";
|
import { infoRouter } from "./router/info";
|
||||||
import { integrationRouter } from "./router/integration/integration-router";
|
import { integrationRouter } from "./router/integration/integration-router";
|
||||||
import { inviteRouter } from "./router/invite";
|
import { inviteRouter } from "./router/invite";
|
||||||
|
import { itemsRouter } from "./router/item/item-router";
|
||||||
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
|
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
|
||||||
import { locationRouter } from "./router/location";
|
import { locationRouter } from "./router/location";
|
||||||
import { logRouter } from "./router/log";
|
import { logRouter } from "./router/log";
|
||||||
@@ -49,6 +50,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
updateChecker: updateCheckerRouter,
|
updateChecker: updateCheckerRouter,
|
||||||
certificates: certificateRouter,
|
certificates: certificateRouter,
|
||||||
info: infoRouter,
|
info: infoRouter,
|
||||||
|
item: itemsRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createId } from "@homarr/common";
|
|||||||
import { asc, eq, inArray, like } from "@homarr/db";
|
import { asc, eq, inArray, like } from "@homarr/db";
|
||||||
import { apps } from "@homarr/db/schema";
|
import { apps } from "@homarr/db/schema";
|
||||||
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
||||||
import { getIconForNameAsync } from "@homarr/icons";
|
import { getIconForName } from "@homarr/icons";
|
||||||
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
|
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
|
||||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||||
|
|
||||||
@@ -141,15 +141,13 @@ export const appRouter = createTRPCRouter({
|
|||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.db.insert(apps).values(
|
await ctx.db.insert(apps).values(
|
||||||
await Promise.all(
|
input.map((app) => ({
|
||||||
input.map(async (app) => ({
|
id: createId(),
|
||||||
id: createId(),
|
name: app.name,
|
||||||
name: app.name,
|
description: app.description,
|
||||||
description: app.description,
|
iconUrl: app.iconUrl ?? getIconForName(ctx.db, app.name).sync()?.url ?? defaultIcon,
|
||||||
iconUrl: app.iconUrl ?? (await getIconForNameAsync(ctx.db, app.name))?.url ?? defaultIcon,
|
href: app.href,
|
||||||
href: app.href,
|
})),
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
update: permissionRequiredProcedure
|
update: permissionRequiredProcedure
|
||||||
|
|||||||
@@ -477,6 +477,47 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.insert(boards)
|
||||||
|
.values({
|
||||||
|
...boardProps,
|
||||||
|
id: newBoardId,
|
||||||
|
name: input.name,
|
||||||
|
creatorId: ctx.session.user.id,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if (layoutsToInsert.length > 0) {
|
||||||
|
transaction.insert(layouts).values(layoutsToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionsToInsert.length > 0) {
|
||||||
|
transaction.insert(sections).values(sectionsToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionLayoutsToInsert.length > 0) {
|
||||||
|
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionCollapseStatesToInsert.length > 0) {
|
||||||
|
transaction.insert(sectionCollapseStates).values(sectionCollapseStatesToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsToInsert.length > 0) {
|
||||||
|
transaction.insert(items).values(itemsToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemLayoutsToInsert.length > 0) {
|
||||||
|
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemIntegrationsToInsert.length > 0) {
|
||||||
|
transaction.insert(integrationItems).values(itemIntegrationsToInsert).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
|
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
|
||||||
@@ -913,6 +954,216 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||||
|
|
||||||
|
if (addedSections.length > 0) {
|
||||||
|
transaction
|
||||||
|
.insert(sections)
|
||||||
|
.values(
|
||||||
|
addedSections.map((section) => ({
|
||||||
|
id: section.id,
|
||||||
|
kind: section.kind,
|
||||||
|
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||||
|
xOffset: section.kind === "dynamic" ? null : 0,
|
||||||
|
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||||
|
name: "name" in section ? section.name : null,
|
||||||
|
boardId: dbBoard.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if (addedSections.some((section) => section.kind === "dynamic")) {
|
||||||
|
transaction
|
||||||
|
.insert(sectionLayouts)
|
||||||
|
.values(
|
||||||
|
addedSections
|
||||||
|
.filter((section) => section.kind === "dynamic")
|
||||||
|
.flatMap((section) =>
|
||||||
|
section.layouts.map(
|
||||||
|
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
|
||||||
|
layoutId: sectionLayout.layoutId,
|
||||||
|
sectionId: section.id,
|
||||||
|
parentSectionId: sectionLayout.parentSectionId,
|
||||||
|
height: sectionLayout.height,
|
||||||
|
width: sectionLayout.width,
|
||||||
|
xOffset: sectionLayout.xOffset,
|
||||||
|
yOffset: sectionLayout.yOffset,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||||
|
|
||||||
|
if (addedItems.length > 0) {
|
||||||
|
transaction
|
||||||
|
.insert(items)
|
||||||
|
.values(
|
||||||
|
addedItems.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
kind: item.kind,
|
||||||
|
options: superjson.stringify(item.options),
|
||||||
|
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||||
|
boardId: dbBoard.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
transaction
|
||||||
|
.insert(itemLayouts)
|
||||||
|
.values(
|
||||||
|
addedItems.flatMap((item) =>
|
||||||
|
item.layouts.map(
|
||||||
|
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||||
|
layoutId: layoutSection.layoutId,
|
||||||
|
sectionId: layoutSection.sectionId,
|
||||||
|
itemId: item.id,
|
||||||
|
height: layoutSection.height,
|
||||||
|
width: layoutSection.width,
|
||||||
|
xOffset: layoutSection.xOffset,
|
||||||
|
yOffset: layoutSection.yOffset,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||||
|
integrationIds.map((integrationId) => ({
|
||||||
|
integrationId,
|
||||||
|
itemId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||||
|
integrationIds.map((integrationId) => ({
|
||||||
|
integrationId,
|
||||||
|
itemId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||||
|
(inputRelation) =>
|
||||||
|
!dbIntegrationRelations.some(
|
||||||
|
(dbRelation) =>
|
||||||
|
dbRelation.itemId === inputRelation.itemId &&
|
||||||
|
dbRelation.integrationId === inputRelation.integrationId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addedIntegrationRelations.length > 0) {
|
||||||
|
transaction
|
||||||
|
.insert(integrationItems)
|
||||||
|
.values(
|
||||||
|
addedIntegrationRelations.map((relation) => ({
|
||||||
|
itemId: relation.itemId,
|
||||||
|
integrationId: relation.integrationId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||||
|
|
||||||
|
for (const item of updatedItems) {
|
||||||
|
transaction
|
||||||
|
.update(items)
|
||||||
|
.set({
|
||||||
|
kind: item.kind,
|
||||||
|
options: superjson.stringify(item.options),
|
||||||
|
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||||
|
})
|
||||||
|
.where(eq(items.id, item.id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
for (const itemSectionLayout of item.layouts) {
|
||||||
|
transaction
|
||||||
|
.update(itemLayouts)
|
||||||
|
.set({
|
||||||
|
height: itemSectionLayout.height,
|
||||||
|
width: itemSectionLayout.width,
|
||||||
|
xOffset: itemSectionLayout.xOffset,
|
||||||
|
yOffset: itemSectionLayout.yOffset,
|
||||||
|
sectionId: itemSectionLayout.sectionId,
|
||||||
|
})
|
||||||
|
.where(and(eq(itemLayouts.itemId, item.id), eq(itemLayouts.layoutId, itemSectionLayout.layoutId)))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||||
|
|
||||||
|
for (const section of updatedSections) {
|
||||||
|
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||||
|
transaction
|
||||||
|
.update(sections)
|
||||||
|
.set({
|
||||||
|
yOffset: prev?.kind !== "dynamic" && "yOffset" in section ? section.yOffset : null,
|
||||||
|
xOffset: prev?.kind !== "dynamic" && "yOffset" in section ? 0 : null,
|
||||||
|
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||||
|
name: prev?.kind === "category" && "name" in section ? section.name : null,
|
||||||
|
})
|
||||||
|
.where(eq(sections.id, section.id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if (section.kind !== "dynamic") continue;
|
||||||
|
|
||||||
|
for (const sectionLayout of section.layouts) {
|
||||||
|
transaction
|
||||||
|
.update(sectionLayouts)
|
||||||
|
.set({
|
||||||
|
height: sectionLayout.height,
|
||||||
|
width: sectionLayout.width,
|
||||||
|
xOffset: sectionLayout.xOffset,
|
||||||
|
yOffset: sectionLayout.yOffset,
|
||||||
|
parentSectionId: sectionLayout.parentSectionId,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(sectionLayouts.sectionId, section.id), eq(sectionLayouts.layoutId, sectionLayout.layoutId)),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||||
|
(dbRelation) =>
|
||||||
|
!inputIntegrationRelations.some(
|
||||||
|
(inputRelation) =>
|
||||||
|
dbRelation.itemId === inputRelation.itemId &&
|
||||||
|
dbRelation.integrationId === inputRelation.integrationId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const relation of removedIntegrationRelations) {
|
||||||
|
transaction
|
||||||
|
.delete(integrationItems)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(integrationItems.itemId, relation.itemId),
|
||||||
|
eq(integrationItems.integrationId, relation.integrationId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||||
|
|
||||||
|
const itemIds = removedItems.map((item) => item.id);
|
||||||
|
if (itemIds.length > 0) {
|
||||||
|
transaction.delete(items).where(inArray(items.id, itemIds)).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||||
|
const sectionIds = removedSections.map((section) => section.id);
|
||||||
|
|
||||||
|
if (sectionIds.length > 0) {
|
||||||
|
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
||||||
@@ -1005,6 +1256,24 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(boardUserPermissions)
|
||||||
|
.values(
|
||||||
|
input.permissions.map((permission) => ({
|
||||||
|
userId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
boardId: input.entityId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
||||||
@@ -1028,6 +1297,24 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(boardGroupPermissions)
|
||||||
|
.values(
|
||||||
|
input.permissions.map((permission) => ({
|
||||||
|
groupId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
boardId: input.entityId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
importOldmarrConfig: permissionRequiredProcedure
|
importOldmarrConfig: permissionRequiredProcedure
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const throwIfActionForbiddenAsync = async (
|
|||||||
ctx: { db: Database; session: Session | null },
|
ctx: { db: Database; session: Session | null },
|
||||||
boardWhere: SQL<unknown>,
|
boardWhere: SQL<unknown>,
|
||||||
permission: BoardPermission,
|
permission: BoardPermission,
|
||||||
|
error?: TRPCError,
|
||||||
) => {
|
) => {
|
||||||
const { db, session } = ctx;
|
const { db, session } = ctx;
|
||||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||||
@@ -40,7 +41,7 @@ export const throwIfActionForbiddenAsync = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!board) {
|
if (!board) {
|
||||||
notAllowed();
|
notAllowed(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasViewAccess, hasChangeAccess, hasFullAccess } = constructBoardPermissions(board, session);
|
const { hasViewAccess, hasChangeAccess, hasFullAccess } = constructBoardPermissions(board, session);
|
||||||
@@ -57,14 +58,18 @@ export const throwIfActionForbiddenAsync = async (
|
|||||||
return; // As view access is required and user has view access, allow
|
return; // As view access is required and user has view access, allow
|
||||||
}
|
}
|
||||||
|
|
||||||
notAllowed();
|
notAllowed(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns NOT_FOUND to prevent snooping on board existence
|
* This method returns NOT_FOUND to prevent snooping on board existence
|
||||||
* A function is used to use the method without return statement
|
* A function is used to use the method without return statement
|
||||||
*/
|
*/
|
||||||
function notAllowed(): never {
|
function notAllowed(error?: TRPCError): never {
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Board not found",
|
message: "Board not found",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
hostname: input.hostname,
|
hostname: input.hostname,
|
||||||
thumbprint: input.thumbprint,
|
thumbprint: input.thumbprint,
|
||||||
});
|
});
|
||||||
await ctx.db
|
const dbResult = await ctx.db
|
||||||
.delete(trustedCertificateHostnames)
|
.delete(trustedCertificateHostnames)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -103,6 +103,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
logger.info("Removed trusted hostname", {
|
logger.info("Removed trusted hostname", {
|
||||||
hostname: input.hostname,
|
hostname: input.hostname,
|
||||||
thumbprint: input.thumbprint,
|
thumbprint: input.thumbprint,
|
||||||
|
count: dbResult.changes,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
removeCertificate: permissionRequiredProcedure
|
removeCertificate: permissionRequiredProcedure
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import type { Database } from "@homarr/db";
|
|||||||
import { and, eq, handleTransactionsAsync, like, not } from "@homarr/db";
|
import { and, eq, handleTransactionsAsync, like, not } from "@homarr/db";
|
||||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||||
import { everyoneGroup } from "@homarr/definitions";
|
import { selectGroupSchema, selectUserSchema } from "@homarr/db/validationSchemas";
|
||||||
|
import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions";
|
||||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||||
import {
|
import {
|
||||||
groupCreateSchema,
|
groupCreateSchema,
|
||||||
@@ -22,33 +23,65 @@ import { throwIfCredentialsDisabled } from "./invite/checks";
|
|||||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||||
|
|
||||||
export const groupRouter = createTRPCRouter({
|
export const groupRouter = createTRPCRouter({
|
||||||
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
getAll: permissionRequiredProcedure
|
||||||
const dbGroups = await ctx.db.query.groups.findMany({
|
.requiresPermission("admin")
|
||||||
with: {
|
.input(z.void())
|
||||||
members: {
|
.output(
|
||||||
with: {
|
z.array(
|
||||||
user: {
|
selectGroupSchema.and(
|
||||||
columns: {
|
z.object({ members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true })) }),
|
||||||
id: true,
|
),
|
||||||
name: true,
|
),
|
||||||
email: true,
|
)
|
||||||
image: true,
|
.meta({
|
||||||
|
openapi: { method: "GET", path: "/api/groups", tags: ["groups"], protect: true, summary: "Retrieve all groups" },
|
||||||
|
})
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const dbGroups = await ctx.db.query.groups.findMany({
|
||||||
|
with: {
|
||||||
|
members: {
|
||||||
|
with: {
|
||||||
|
user: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return dbGroups.map((group) => ({
|
return dbGroups.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
members: group.members.map((member) => member.user),
|
members: group.members.map((member) => member.user),
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPaginated: permissionRequiredProcedure
|
getPaginated: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(paginatedSchema)
|
.input(paginatedSchema)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
items: z.array(
|
||||||
|
selectGroupSchema.and(
|
||||||
|
z.object({ members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true })) }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
totalCount: z.number(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/groups/paginated",
|
||||||
|
tags: ["groups"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Retrieve groups with pagination",
|
||||||
|
},
|
||||||
|
})
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||||
const groupCount = await ctx.db.$count(groups, whereQuery);
|
const groupCount = await ctx.db.$count(groups, whereQuery);
|
||||||
@@ -84,6 +117,24 @@ export const groupRouter = createTRPCRouter({
|
|||||||
getById: permissionRequiredProcedure
|
getById: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(byIdSchema)
|
.input(byIdSchema)
|
||||||
|
.output(
|
||||||
|
selectGroupSchema.and(
|
||||||
|
z.object({
|
||||||
|
owner: selectUserSchema.pick({ id: true, name: true, image: true, email: true }).nullable(),
|
||||||
|
members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true, provider: true })),
|
||||||
|
permissions: z.array(z.enum(groupPermissionKeys)),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/groups/{id}",
|
||||||
|
tags: ["groups"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Retrieve group details",
|
||||||
|
},
|
||||||
|
})
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const group = await ctx.db.query.groups.findFirst({
|
const group = await ctx.db.query.groups.findFirst({
|
||||||
where: eq(groups.id, input.id),
|
where: eq(groups.id, input.id),
|
||||||
@@ -201,6 +252,10 @@ export const groupRouter = createTRPCRouter({
|
|||||||
createGroup: permissionRequiredProcedure
|
createGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(groupCreateSchema)
|
.input(groupCreateSchema)
|
||||||
|
.output(z.string())
|
||||||
|
.meta({
|
||||||
|
openapi: { method: "POST", path: "/api/groups", tags: ["groups"], protect: true, summary: "Create group" },
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||||
|
|
||||||
@@ -219,6 +274,10 @@ export const groupRouter = createTRPCRouter({
|
|||||||
updateGroup: permissionRequiredProcedure
|
updateGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(groupUpdateSchema)
|
.input(groupUpdateSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.meta({
|
||||||
|
openapi: { method: "PUT", path: "/api/groups/{id}", tags: ["groups"], protect: true, summary: "Update group" },
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
@@ -260,6 +319,13 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync: (db) => {
|
||||||
|
db.transaction((trx) => {
|
||||||
|
for (const { id, position } of positions) {
|
||||||
|
trx.update(groups).set({ position }).where(eq(groups.id, id)).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
savePermissions: permissionRequiredProcedure
|
savePermissions: permissionRequiredProcedure
|
||||||
@@ -296,6 +362,10 @@ export const groupRouter = createTRPCRouter({
|
|||||||
deleteGroup: permissionRequiredProcedure
|
deleteGroup: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(byIdSchema)
|
.input(byIdSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.meta({
|
||||||
|
openapi: { method: "DELETE", path: "/api/groups/{id}", tags: ["groups"], protect: true, summary: "Delete group" },
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||||
@@ -305,6 +375,16 @@ export const groupRouter = createTRPCRouter({
|
|||||||
addMember: permissionRequiredProcedure
|
addMember: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(groupUserSchema)
|
.input(groupUserSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/api/groups/{groupId}/members/{userId}",
|
||||||
|
tags: ["groups"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Add member to group",
|
||||||
|
},
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
@@ -321,6 +401,8 @@ export const groupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Create another pr to only allow adding users with provider=credentials
|
||||||
|
|
||||||
await ctx.db.insert(groupMembers).values({
|
await ctx.db.insert(groupMembers).values({
|
||||||
groupId: input.groupId,
|
groupId: input.groupId,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
@@ -329,6 +411,16 @@ export const groupRouter = createTRPCRouter({
|
|||||||
removeMember: permissionRequiredProcedure
|
removeMember: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(groupUserSchema)
|
.input(groupUserSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/api/groups/{groupId}/members/{userId}",
|
||||||
|
tags: ["groups"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Remove member from group",
|
||||||
|
},
|
||||||
|
})
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||||
|
|||||||
@@ -541,6 +541,27 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.delete(integrationUserPermissions)
|
||||||
|
.where(eq(integrationUserPermissions.integrationId, input.entityId))
|
||||||
|
.run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(integrationUserPermissions)
|
||||||
|
.values(
|
||||||
|
input.permissions.map((permission) => ({
|
||||||
|
userId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
integrationId: input.entityId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
saveGroupIntegrationPermissions: protectedProcedure
|
saveGroupIntegrationPermissions: protectedProcedure
|
||||||
@@ -566,6 +587,27 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.delete(integrationGroupPermissions)
|
||||||
|
.where(eq(integrationGroupPermissions.integrationId, input.entityId))
|
||||||
|
.run();
|
||||||
|
if (input.permissions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
.insert(integrationGroupPermissions)
|
||||||
|
.values(
|
||||||
|
input.permissions.map((permission) => ({
|
||||||
|
groupId: permission.principalId,
|
||||||
|
permission: permission.permission,
|
||||||
|
integrationId: input.entityId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
searchInIntegration: protectedProcedure
|
searchInIntegration: protectedProcedure
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import SuperJSON from "superjson";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { createId, objectEntries } from "@homarr/common";
|
||||||
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
|
import { and, eq, handleTransactionsAsync } from "@homarr/db";
|
||||||
|
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
||||||
|
import { boards, itemLayouts, items } from "@homarr/db/schema";
|
||||||
|
import { selectItemLayoutSchema, selectitemSchema } from "@homarr/db/validationSchemas";
|
||||||
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
|
import { widgetKinds } from "@homarr/definitions";
|
||||||
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
|
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||||
|
import { itemAdvancedOptionsSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
|
import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "../../../../widgets/src";
|
||||||
|
import type { WidgetOptionDefinition } from "../../../../widgets/src/options";
|
||||||
|
import { convertIntersectionToZodObject } from "../../schema-merger";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||||
|
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
||||||
|
|
||||||
|
const createItemOptionsSchema = (kind: WidgetKind): z.ZodObject =>
|
||||||
|
z.object(
|
||||||
|
objectEntries(
|
||||||
|
widgetImports[kind].definition.createOptions({
|
||||||
|
enableStatusByDefault: true,
|
||||||
|
firstDayOfWeek: 1,
|
||||||
|
forceDisableStatus: false,
|
||||||
|
}),
|
||||||
|
).reduce(
|
||||||
|
(previous, [key, value]: [string, WidgetOptionDefinition]) => {
|
||||||
|
previous[key] = value.validate;
|
||||||
|
return previous;
|
||||||
|
},
|
||||||
|
{} as Record<string, z.ZodType>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputItemSchema = z.union([
|
||||||
|
...widgetKinds.map((kind) =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
kind: z.literal(kind),
|
||||||
|
options: createItemOptionsSchema(kind),
|
||||||
|
advancedOptions: itemAdvancedOptionsSchema,
|
||||||
|
layouts: z.array(selectItemLayoutSchema.omit({ itemId: true })),
|
||||||
|
})
|
||||||
|
.and(
|
||||||
|
selectitemSchema.pick({
|
||||||
|
id: true,
|
||||||
|
boardId: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const manageItemSchema = z.object({
|
||||||
|
boardId: z.string(),
|
||||||
|
kind: zodEnumFromArray(widgetKinds),
|
||||||
|
options: z.record(z.string(), z.unknown().optional()),
|
||||||
|
advancedOptions: itemAdvancedOptionsSchema,
|
||||||
|
layouts: z.array(selectItemLayoutSchema.omit({ itemId: true })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const itemsRouter = createTRPCRouter({
|
||||||
|
getAll: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
boardId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(z.array(outputItemSchema))
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/boards/{boardId}/items",
|
||||||
|
tags: ["items"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Retrieve all items for board",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const board = await ctx.db.query.boards.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.id, input.boardId),
|
||||||
|
with: {
|
||||||
|
items: {
|
||||||
|
with: {
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Board not found",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!board) throw error;
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "view", error);
|
||||||
|
|
||||||
|
return board.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
options: reduceWidgetOptionsWithDefaultValues(
|
||||||
|
item.kind,
|
||||||
|
{
|
||||||
|
enableStatusByDefault: true,
|
||||||
|
firstDayOfWeek: 1,
|
||||||
|
forceDisableStatus: false,
|
||||||
|
},
|
||||||
|
SuperJSON.parse(item.options),
|
||||||
|
),
|
||||||
|
advancedOptions: itemAdvancedOptionsSchema.parse(SuperJSON.parse(item.advancedOptions)),
|
||||||
|
layouts: item.layouts.map(({ itemId: _, ...layout }) => layout),
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
createItem: protectedProcedure
|
||||||
|
.input(manageItemSchema)
|
||||||
|
.output(outputItemSchema)
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/api/items",
|
||||||
|
tags: ["items"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Create item on board",
|
||||||
|
description: "Options available can be viewed in response options object. Input options are optional",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const board = await ctx.db.query.boards.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.id, input.boardId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Board not found",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!board) throw error;
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, board.id), "modify", error);
|
||||||
|
|
||||||
|
const itemOptionsSchema = createItemOptionsSchema(input.kind);
|
||||||
|
const fullOptions = reduceWidgetOptionsWithDefaultValues(
|
||||||
|
input.kind,
|
||||||
|
{ enableStatusByDefault: true, firstDayOfWeek: 1, forceDisableStatus: false },
|
||||||
|
input.options,
|
||||||
|
);
|
||||||
|
const result = await itemOptionsSchema.safeParseAsync(fullOptions);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Failed to parse item options",
|
||||||
|
cause: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: createId(),
|
||||||
|
boardId: input.boardId,
|
||||||
|
kind: input.kind,
|
||||||
|
advancedOptions: input.advancedOptions,
|
||||||
|
options: result.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemInsert: InferInsertModel<typeof items> = {
|
||||||
|
...item,
|
||||||
|
advancedOptions: SuperJSON.stringify(item.advancedOptions),
|
||||||
|
options: SuperJSON.stringify(item.options),
|
||||||
|
};
|
||||||
|
const layoutInserts: InferInsertModel<typeof itemLayouts>[] = input.layouts.map((layout) => ({
|
||||||
|
...layout,
|
||||||
|
itemId: item.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const insertCollection = createDbInsertCollectionWithoutTransaction(["items", "itemLayouts"]);
|
||||||
|
insertCollection.items.push(itemInsert);
|
||||||
|
insertCollection.itemLayouts.push(...layoutInserts);
|
||||||
|
await insertCollection.insertAllAsync(ctx.db);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
layouts: layoutInserts,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
getItem: protectedProcedure
|
||||||
|
.input(byIdSchema)
|
||||||
|
.output(outputItemSchema)
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/items/{id}",
|
||||||
|
tags: ["items"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Retrieve item from a board",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const item = await ctx.db.query.items.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.id, input.id),
|
||||||
|
with: {
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Item not found",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) throw error;
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "view", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
options: reduceWidgetOptionsWithDefaultValues(
|
||||||
|
item.kind,
|
||||||
|
{
|
||||||
|
enableStatusByDefault: true,
|
||||||
|
firstDayOfWeek: 1,
|
||||||
|
forceDisableStatus: false,
|
||||||
|
},
|
||||||
|
SuperJSON.parse(item.options),
|
||||||
|
),
|
||||||
|
advancedOptions: itemAdvancedOptionsSchema.parse(SuperJSON.parse(item.advancedOptions)),
|
||||||
|
layouts: item.layouts.map(({ itemId: _, ...layout }) => layout),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
updateItem: protectedProcedure
|
||||||
|
.input(convertIntersectionToZodObject(manageItemSchema.omit({ boardId: true, kind: true }).and(byIdSchema)))
|
||||||
|
.output(outputItemSchema)
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "PUT",
|
||||||
|
path: "/api/items/{id}",
|
||||||
|
tags: ["items"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Update an item on the board",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const item = await ctx.db.query.items.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.id, input.id),
|
||||||
|
with: {
|
||||||
|
layouts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Item not found",
|
||||||
|
});
|
||||||
|
if (!item) throw error;
|
||||||
|
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "modify", error);
|
||||||
|
|
||||||
|
const previousOptions = SuperJSON.parse<Record<string, unknown>>(item.options);
|
||||||
|
const combinedOptions = {
|
||||||
|
...previousOptions,
|
||||||
|
...input.options,
|
||||||
|
};
|
||||||
|
const itemOptionsSchema = createItemOptionsSchema(item.kind);
|
||||||
|
const fullOptions = reduceWidgetOptionsWithDefaultValues(
|
||||||
|
item.kind,
|
||||||
|
{ enableStatusByDefault: true, firstDayOfWeek: 1, forceDisableStatus: false },
|
||||||
|
combinedOptions,
|
||||||
|
);
|
||||||
|
const optionsResult = await itemOptionsSchema.safeParseAsync(fullOptions);
|
||||||
|
if (!optionsResult.success) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Failed to parse item options",
|
||||||
|
cause: optionsResult.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleTransactionsAsync(ctx.db, {
|
||||||
|
async handleAsync(db, schema) {
|
||||||
|
await db.transaction(async (transaction) => {
|
||||||
|
await transaction
|
||||||
|
.update(schema.items)
|
||||||
|
.set({
|
||||||
|
advancedOptions: SuperJSON.stringify(input.advancedOptions),
|
||||||
|
options: SuperJSON.stringify(optionsResult),
|
||||||
|
})
|
||||||
|
.where(eq(schema.items.id, input.id));
|
||||||
|
|
||||||
|
for (const itemLayout of input.layouts) {
|
||||||
|
await transaction
|
||||||
|
.update(schema.itemLayouts)
|
||||||
|
.set({
|
||||||
|
height: itemLayout.height,
|
||||||
|
width: itemLayout.width,
|
||||||
|
sectionId: itemLayout.sectionId,
|
||||||
|
xOffset: itemLayout.xOffset,
|
||||||
|
yOffset: itemLayout.yOffset,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(eq(schema.itemLayouts.itemId, input.id), eq(schema.itemLayouts.layoutId, itemLayout.layoutId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.update(items)
|
||||||
|
.set({
|
||||||
|
advancedOptions: SuperJSON.stringify(input.advancedOptions),
|
||||||
|
options: SuperJSON.stringify(optionsResult),
|
||||||
|
})
|
||||||
|
.where(eq(items.id, input.id));
|
||||||
|
|
||||||
|
for (const itemLayout of input.layouts) {
|
||||||
|
transaction
|
||||||
|
.update(itemLayouts)
|
||||||
|
.set({
|
||||||
|
height: itemLayout.height,
|
||||||
|
width: itemLayout.width,
|
||||||
|
sectionId: itemLayout.sectionId,
|
||||||
|
xOffset: itemLayout.xOffset,
|
||||||
|
yOffset: itemLayout.yOffset,
|
||||||
|
})
|
||||||
|
.where(and(eq(itemLayouts.itemId, input.id), eq(itemLayouts.layoutId, itemLayout.layoutId)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: input.id,
|
||||||
|
kind: item.kind,
|
||||||
|
boardId: item.boardId,
|
||||||
|
advancedOptions: input.advancedOptions,
|
||||||
|
options: optionsResult,
|
||||||
|
layouts: input.layouts,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
deleteItem: protectedProcedure
|
||||||
|
.input(byIdSchema)
|
||||||
|
.output(z.void())
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/api/items/{id}",
|
||||||
|
tags: ["items"],
|
||||||
|
protect: true,
|
||||||
|
summary: "Delete an item from board",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const item = await ctx.db.query.items.findFirst({
|
||||||
|
where: (fields, { eq }) => eq(fields.id, input.id),
|
||||||
|
});
|
||||||
|
const error = new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Item not found",
|
||||||
|
});
|
||||||
|
if (!item) throw error;
|
||||||
|
|
||||||
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "modify", error);
|
||||||
|
|
||||||
|
await ctx.db.delete(items).where(eq(items.id, input.id));
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@libsql/client": "^0.15.15",
|
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
|
"better-sqlite3": "^12.5.0",
|
||||||
"dns-caching": "^0.2.9",
|
"dns-caching": "^0.2.9",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.8.2",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createClient } from "@libsql/client";
|
import Database from "better-sqlite3";
|
||||||
import { drizzle as drizzleSqlite } from "drizzle-orm/libsql";
|
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
||||||
|
|
||||||
import { dbEnv } from "../env";
|
import { dbEnv } from "../env";
|
||||||
import type { SharedDrizzleConfig } from "./shared";
|
import type { SharedDrizzleConfig } from "./shared";
|
||||||
|
|
||||||
export const createSqliteDb = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
|
export const createSqliteDb = <TSchema extends Record<string, unknown>>(config: SharedDrizzleConfig<TSchema>) => {
|
||||||
const client = createClient({ url: `file:${dbEnv.URL}` });
|
const connection = new Database(dbEnv.URL);
|
||||||
return drizzleSqlite<TSchema>(client, config);
|
return drizzleSqlite<TSchema>(connection, config);
|
||||||
};
|
};
|
||||||
@@ -121,6 +121,46 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countDeleted += deadIcons.length;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync() {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
if (newIconRepositories.length >= 1) {
|
||||||
|
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIcons.length >= 1) {
|
||||||
|
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||||
|
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||||
|
transaction.insert(icons).values(chunck).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deadIcons.length >= 1) {
|
||||||
|
transaction
|
||||||
|
.delete(icons)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||||
|
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||||
|
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deadIconRepositories.length >= 1) {
|
||||||
|
transaction
|
||||||
|
.delete(iconRepositories)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
iconRepositories.id,
|
||||||
|
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
countDeleted += deadIcons.length;
|
countDeleted += deadIcons.length;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,8 +31,20 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
|
insertAll: (db: HomarrDatabase) => {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
for (const [key, values] of objectEntries(context)) {
|
||||||
|
if (values.length >= 1) {
|
||||||
|
transaction
|
||||||
|
.insert(schema[key])
|
||||||
|
.values(values as never)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
// We allow any database that supports async passed here but then fallback to mysql to prevent typescript errors
|
// We allow any database that supports async passed here but then fallback to mysql to prevent typescript errors
|
||||||
insertAllAsync: async (db: HomarrDatabaseMysql | HomarrDatabasePostgresql | HomarrDatabase) => {
|
insertAllAsync: async (db: HomarrDatabaseMysql | HomarrDatabasePostgresql) => {
|
||||||
const innerDb = db as HomarrDatabaseMysql;
|
const innerDb = db as HomarrDatabaseMysql;
|
||||||
await innerDb.transaction(async (transaction) => {
|
await innerDb.transaction(async (transaction) => {
|
||||||
for (const [key, values] of objectEntries(context)) {
|
for (const [key, values] of objectEntries(context)) {
|
||||||
@@ -49,7 +61,7 @@ export const createDbInsertCollectionForTransaction = <TTableKey extends TableKe
|
|||||||
export const createDbInsertCollectionWithoutTransaction = <TTableKey extends TableKey>(
|
export const createDbInsertCollectionWithoutTransaction = <TTableKey extends TableKey>(
|
||||||
tablesInInsertOrder: TTableKey[],
|
tablesInInsertOrder: TTableKey[],
|
||||||
) => {
|
) => {
|
||||||
const { insertAllAsync, ...collection } = createDbInsertCollectionForTransaction(tablesInInsertOrder);
|
const { insertAll, insertAllAsync, ...collection } = createDbInsertCollectionForTransaction(tablesInInsertOrder);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
@@ -63,7 +75,7 @@ export const createDbInsertCollectionWithoutTransaction = <TTableKey extends Tab
|
|||||||
default:
|
default:
|
||||||
// For better-sqlite3, we need to use the synchronous insertAll method
|
// For better-sqlite3, we need to use the synchronous insertAll method
|
||||||
// default assumes better-sqlite3. It's original implementation.
|
// default assumes better-sqlite3. It's original implementation.
|
||||||
await insertAllAsync(db);
|
insertAll(db);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||||
import type { MySql2Database } from "drizzle-orm/mysql2";
|
import type { MySql2Database } from "drizzle-orm/mysql2";
|
||||||
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||||
|
|
||||||
@@ -6,6 +6,6 @@ import type * as mysqlSchema from "./schema/mysql";
|
|||||||
import type * as pgSchema from "./schema/postgresql";
|
import type * as pgSchema from "./schema/postgresql";
|
||||||
import type * as sqliteSchema from "./schema/sqlite";
|
import type * as sqliteSchema from "./schema/sqlite";
|
||||||
|
|
||||||
export type HomarrDatabase = LibSQLDatabase<typeof sqliteSchema>;
|
export type HomarrDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
||||||
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
|
export type HomarrDatabaseMysql = MySql2Database<typeof mysqlSchema>;
|
||||||
export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>;
|
export type HomarrDatabasePostgresql = NodePgDatabase<typeof pgSchema>;
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"@testcontainers/mysql": "^11.10.0",
|
"@testcontainers/mysql": "^11.10.0",
|
||||||
"@testcontainers/postgresql": "^11.10.0",
|
"@testcontainers/postgresql": "^11.10.0",
|
||||||
|
"better-sqlite3": "^12.5.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-kit": "^0.31.8",
|
"drizzle-kit": "^0.31.8",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.27.1",
|
"esbuild": "^0.27.1",
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { createClient } from "@libsql/client";
|
import Database from "better-sqlite3";
|
||||||
import { drizzle } from "drizzle-orm/libsql";
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
import { migrate } from "drizzle-orm/libsql/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
|
||||||
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
|
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
|
||||||
|
|
||||||
import * as sqliteSchema from "../schema/sqlite";
|
import * as sqliteSchema from "../schema/sqlite";
|
||||||
|
|
||||||
export const createDb = (debug?: boolean) => {
|
export const createDb = (debug?: boolean) => {
|
||||||
const sqlite = createClient({ url: ":memory:" });
|
const sqlite = new Database(":memory:");
|
||||||
const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: DB_CASING });
|
const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: DB_CASING });
|
||||||
// TODO: Make async and awaitable
|
migrate(db, {
|
||||||
void migrate(db, {
|
|
||||||
migrationsFolder: "./packages/db/migrations/sqlite",
|
migrationsFolder: "./packages/db/migrations/sqlite",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { isMysql, isPostgresql } from "./collection";
|
||||||
import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
|
import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
|
||||||
import type { MySqlSchema } from "./schema";
|
import type { MySqlSchema } from "./schema";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
|
|
||||||
interface HandleTransactionInput {
|
interface HandleTransactionInput {
|
||||||
handleAsync: (db: HomarrDatabaseMysql, schema: MySqlSchema) => Promise<void>;
|
handleAsync: (db: HomarrDatabaseMysql, schema: MySqlSchema) => Promise<void>;
|
||||||
|
handleSync: (db: HomarrDatabase) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,5 +14,10 @@ interface HandleTransactionInput {
|
|||||||
* But it can also generally be used when dealing with different database drivers.
|
* But it can also generally be used when dealing with different database drivers.
|
||||||
*/
|
*/
|
||||||
export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => {
|
export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => {
|
||||||
await input.handleAsync(db as unknown as HomarrDatabaseMysql, schema as unknown as MySqlSchema);
|
if (isMysql() || isPostgresql()) {
|
||||||
|
// Schema type is always the correct one based on env variables
|
||||||
|
await input.handleAsync(db as unknown as HomarrDatabaseMysql, schema as unknown as MySqlSchema);
|
||||||
|
} else {
|
||||||
|
input.handleSync(db);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { createSelectSchema } from "drizzle-zod";
|
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||||
|
|
||||||
import { apps, boards, groups, invites, searchEngines, serverSettings, users } from "./schema";
|
import { apps, boards, groups, invites, itemLayouts, items, searchEngines, serverSettings, users } from "./schema";
|
||||||
|
|
||||||
export const selectAppSchema = createSelectSchema(apps);
|
export const selectAppSchema = createSelectSchema(apps);
|
||||||
export const selectBoardSchema = createSelectSchema(boards);
|
export const selectBoardSchema = createSelectSchema(boards);
|
||||||
|
export const selectitemSchema = createSelectSchema(items);
|
||||||
|
export const insertItemSchema = createInsertSchema(items);
|
||||||
|
export const selectItemLayoutSchema = createSelectSchema(itemLayouts);
|
||||||
export const selectGroupSchema = createSelectSchema(groups);
|
export const selectGroupSchema = createSelectSchema(groups);
|
||||||
export const selectInviteSchema = createSelectSchema(invites);
|
export const selectInviteSchema = createSelectSchema(invites);
|
||||||
export const selectSearchEnginesSchema = createSelectSchema(searchEngines);
|
export const selectSearchEnginesSchema = createSelectSchema(searchEngines);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import type { Database } from "@homarr/db";
|
|||||||
import { like } from "@homarr/db";
|
import { like } from "@homarr/db";
|
||||||
import { icons } from "@homarr/db/schema";
|
import { icons } from "@homarr/db/schema";
|
||||||
|
|
||||||
export const getIconForNameAsync = async (db: Database, name: string) => {
|
export const getIconForName = (db: Database, name: string) => {
|
||||||
return await db.query.icons.findFirst({
|
return db.query.icons.findFirst({
|
||||||
where: like(icons.name, `%${name}%`),
|
where: like(icons.name, `%${name}%`),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -45,6 +45,13 @@ export const importInitialOldmarrAsync = async (
|
|||||||
await integrationInsertCollection.insertAllAsync(transaction);
|
await integrationInsertCollection.insertAllAsync(transaction);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((transaction) => {
|
||||||
|
boardInsertCollection.insertAll(transaction);
|
||||||
|
userInsertCollection.insertAll(transaction);
|
||||||
|
integrationInsertCollection.insertAll(transaction);
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("Import successful", { duration: stopwatch.getElapsedInHumanWords() });
|
logger.info("Import successful", { duration: stopwatch.getElapsedInHumanWords() });
|
||||||
|
|||||||
@@ -35,5 +35,8 @@ export const importSingleOldmarrConfigAsync = async (
|
|||||||
async handleAsync(db) {
|
async handleAsync(db) {
|
||||||
await boardInsertCollection.insertAllAsync(db);
|
await boardInsertCollection.insertAllAsync(db);
|
||||||
},
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
boardInsertCollection.insertAll(db);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -57,12 +57,7 @@ export const createRequestIntegrationJobHandler = <
|
|||||||
reduceWidgetOptionsWithDefaultValues(
|
reduceWidgetOptionsWithDefaultValues(
|
||||||
itemForIntegration.kind,
|
itemForIntegration.kind,
|
||||||
{
|
{
|
||||||
defaultSearchEngineId: serverSettings.search.defaultSearchEngineId,
|
|
||||||
openSearchInNewTab: true,
|
|
||||||
firstDayOfWeek: 1,
|
firstDayOfWeek: 1,
|
||||||
homeBoardId: serverSettings.board.homeBoardId,
|
|
||||||
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
|
|
||||||
pingIconsEnabled: true,
|
|
||||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,24 +27,13 @@ export const WidgetMultiTextInput = ({ property, kind, options }: CommonWidgetIn
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentValidationResult = React.useMemo(() => {
|
const currentValidationResult = React.useMemo(() => {
|
||||||
if (!options.validate) {
|
return options.validate.safeParse(search);
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
result: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationResult = options.validate.safeParse(search);
|
|
||||||
return {
|
|
||||||
success: validationResult.success,
|
|
||||||
result: validationResult,
|
|
||||||
};
|
|
||||||
}, [options.validate, search]);
|
}, [options.validate, search]);
|
||||||
|
|
||||||
const error = React.useMemo(() => {
|
const error = React.useMemo(() => {
|
||||||
/* hide the error when nothing is being typed since "" is not valid but is not an explicit error */
|
/* hide the error when nothing is being typed since "" is not valid but is not an explicit error */
|
||||||
if (!currentValidationResult.success && currentValidationResult.result && search.length !== 0) {
|
if (!currentValidationResult.success && search.length !== 0) {
|
||||||
return currentValidationResult.result.error?.issues[0]?.message;
|
return currentValidationResult.error.issues[0]?.message;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [currentValidationResult, search]);
|
}, [currentValidationResult, search]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ActionIcon, Avatar, Group, Stack, Text } from "@mantine/core";
|
import { ActionIcon, Avatar, Group, Stack, Text } from "@mantine/core";
|
||||||
import { IconBookmark, IconX } from "@tabler/icons-react";
|
import { IconBookmark, IconX } from "@tabler/icons-react";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -56,6 +57,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
|
|||||||
isLoading,
|
isLoading,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
validate: z.array(z.string()),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export interface WidgetDefinition {
|
|||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
supportedIntegrations?: IntegrationKind[];
|
supportedIntegrations?: IntegrationKind[];
|
||||||
integrationsRequired?: boolean;
|
integrationsRequired?: boolean;
|
||||||
createOptions: (settings: SettingsContextProps) => WidgetOptionsRecord;
|
createOptions: (
|
||||||
|
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "firstDayOfWeek" | "forceDisableStatus">,
|
||||||
|
) => WidgetOptionsRecord;
|
||||||
errors?: Partial<
|
errors?: Partial<
|
||||||
Record<
|
Record<
|
||||||
DefaultErrorData["code"],
|
DefaultErrorData["code"],
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (Widget
|
|||||||
|
|
||||||
export const reduceWidgetOptionsWithDefaultValues = (
|
export const reduceWidgetOptionsWithDefaultValues = (
|
||||||
kind: WidgetKind,
|
kind: WidgetKind,
|
||||||
settings: SettingsContextProps,
|
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "firstDayOfWeek" | "forceDisableStatus">,
|
||||||
currentValue: Record<string, unknown> = {},
|
currentValue: Record<string, unknown> = {},
|
||||||
) => {
|
) => {
|
||||||
const definition = widgetImports[kind].definition;
|
const definition = widgetImports[kind].definition;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod/v4";
|
|||||||
import type { ZodType } from "zod/v4";
|
import type { ZodType } from "zod/v4";
|
||||||
|
|
||||||
import type { IntegrationKind } from "@homarr/definitions";
|
import type { IntegrationKind } from "@homarr/definitions";
|
||||||
|
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||||
|
|
||||||
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
|
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
|
||||||
import type { ReleasesRepository } from "./releases/releases-repository";
|
import type { ReleasesRepository } from "./releases/releases-repository";
|
||||||
@@ -38,6 +39,7 @@ export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentif
|
|||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
uniqueIdentifier: (item: TItem) => TOptionValue;
|
uniqueIdentifier: (item: TItem) => TOptionValue;
|
||||||
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
|
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
|
||||||
|
validate: z.ZodArray<z.ZodType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
|
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
|
||||||
@@ -68,12 +70,13 @@ const optionsFactory = {
|
|||||||
type: "switch" as const,
|
type: "switch" as const,
|
||||||
defaultValue: input?.defaultValue ?? false,
|
defaultValue: input?.defaultValue ?? false,
|
||||||
withDescription: input?.withDescription ?? false,
|
withDescription: input?.withDescription ?? false,
|
||||||
|
validate: z.boolean(),
|
||||||
}),
|
}),
|
||||||
text: (input?: TextInput) => ({
|
text: (input?: TextInput) => ({
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
defaultValue: input?.defaultValue ?? "",
|
defaultValue: input?.defaultValue ?? "",
|
||||||
withDescription: input?.withDescription ?? false,
|
withDescription: input?.withDescription ?? false,
|
||||||
validate: input?.validate,
|
validate: input?.validate ?? z.string(),
|
||||||
}),
|
}),
|
||||||
multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({
|
multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({
|
||||||
type: "multiSelect" as const,
|
type: "multiSelect" as const,
|
||||||
@@ -81,6 +84,9 @@ const optionsFactory = {
|
|||||||
options: input.options,
|
options: input.options,
|
||||||
searchable: input.searchable ?? false,
|
searchable: input.searchable ?? false,
|
||||||
withDescription: input.withDescription ?? false,
|
withDescription: input.withDescription ?? false,
|
||||||
|
validate: z.array(
|
||||||
|
zodEnumFromArray(input.options.map((option) => (typeof option === "string" ? option : option.value))),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
|
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
|
||||||
type: "select" as const,
|
type: "select" as const,
|
||||||
@@ -88,6 +94,7 @@ const optionsFactory = {
|
|||||||
options: input.options,
|
options: input.options,
|
||||||
searchable: input.searchable ?? false,
|
searchable: input.searchable ?? false,
|
||||||
withDescription: input.withDescription ?? false,
|
withDescription: input.withDescription ?? false,
|
||||||
|
validate: zodEnumFromArray(input.options.map((option) => (typeof option === "string" ? option : option.value))),
|
||||||
}),
|
}),
|
||||||
number: (input: NumberInput) => ({
|
number: (input: NumberInput) => ({
|
||||||
type: "number" as const,
|
type: "number" as const,
|
||||||
@@ -122,19 +129,20 @@ const optionsFactory = {
|
|||||||
defaultValue: input?.defaultValue ?? [],
|
defaultValue: input?.defaultValue ?? [],
|
||||||
withDescription: input?.withDescription ?? false,
|
withDescription: input?.withDescription ?? false,
|
||||||
values: [] as string[],
|
values: [] as string[],
|
||||||
validate: input?.validate,
|
validate: input?.validate ?? z.array(z.string()),
|
||||||
}),
|
}),
|
||||||
multiReleasesRepositories: (input?: CommonInput<ReleasesRepository[]> & { validate?: ZodType }) => ({
|
multiReleasesRepositories: (input: CommonInput<ReleasesRepository[]> & { validate: ZodType }) => ({
|
||||||
type: "multiReleasesRepositories" as const,
|
type: "multiReleasesRepositories" as const,
|
||||||
defaultValue: input?.defaultValue ?? [],
|
defaultValue: input.defaultValue ?? [],
|
||||||
withDescription: input?.withDescription ?? false,
|
withDescription: input.withDescription ?? false,
|
||||||
values: [] as ReleasesRepository[],
|
values: [] as ReleasesRepository[],
|
||||||
validate: input?.validate,
|
validate: input.validate,
|
||||||
}),
|
}),
|
||||||
app: () => ({
|
app: () => ({
|
||||||
type: "app" as const,
|
type: "app" as const,
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
withDescription: false,
|
withDescription: false,
|
||||||
|
validate: z.string(),
|
||||||
}),
|
}),
|
||||||
sortableItemList: <const TItem, const TOptionValue extends UniqueIdentifier>(
|
sortableItemList: <const TItem, const TOptionValue extends UniqueIdentifier>(
|
||||||
input: SortableItemListInput<TItem, TOptionValue>,
|
input: SortableItemListInput<TItem, TOptionValue>,
|
||||||
@@ -146,6 +154,7 @@ const optionsFactory = {
|
|||||||
uniqueIdentifier: input.uniqueIdentifier,
|
uniqueIdentifier: input.uniqueIdentifier,
|
||||||
useData: input.useData,
|
useData: input.useData,
|
||||||
withDescription: false,
|
withDescription: false,
|
||||||
|
validate: input.validate,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Generated
+1755
-1281
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user