mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-02 01:30:38 -06:00
added domain tab in app
This commit is contained in:
38
prisma/migrations/20241025084526_migration/migration.sql
Normal file
38
prisma/migrations/20241025084526_migration/migration.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `AppDomain` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- The primary key for the `AppVolume` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- The required column `id` was added to the `AppDomain` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
- The required column `id` was added to the `AppVolume` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_AppDomain" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"port" INTEGER NOT NULL,
|
||||
"useSsl" BOOLEAN NOT NULL DEFAULT true,
|
||||
"appId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "AppDomain_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AppDomain" ("appId", "createdAt", "hostname", "port", "updatedAt", "useSsl") SELECT "appId", "createdAt", "hostname", "port", "updatedAt", "useSsl" FROM "AppDomain";
|
||||
DROP TABLE "AppDomain";
|
||||
ALTER TABLE "new_AppDomain" RENAME TO "AppDomain";
|
||||
CREATE TABLE "new_AppVolume" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"containerMountPath" TEXT NOT NULL,
|
||||
"appId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "AppVolume_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_AppVolume" ("appId", "containerMountPath", "createdAt", "updatedAt") SELECT "appId", "containerMountPath", "createdAt", "updatedAt" FROM "AppVolume";
|
||||
DROP TABLE "AppVolume";
|
||||
ALTER TABLE "new_AppVolume" RENAME TO "AppVolume";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
8
prisma/migrations/20241025085330_migration/migration.sql
Normal file
8
prisma/migrations/20241025085330_migration/migration.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[hostname]` on the table `AppDomain` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AppDomain_hostname_key" ON "AppDomain"("hostname");
|
||||
@@ -152,7 +152,8 @@ model App {
|
||||
}
|
||||
|
||||
model AppDomain {
|
||||
hostname String
|
||||
id String @id @default(uuid())
|
||||
hostname String @unique
|
||||
port Int
|
||||
useSsl Boolean @default(true)
|
||||
appId String
|
||||
@@ -160,17 +161,14 @@ model AppDomain {
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([hostname, appId])
|
||||
}
|
||||
|
||||
model AppVolume {
|
||||
id String @id @default(uuid())
|
||||
containerMountPath String
|
||||
appId String
|
||||
app App @relation(fields: [appId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([containerMountPath, appId])
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
'use server'
|
||||
|
||||
import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/model/env-edit.model";
|
||||
import { appDomainEditZodModel } from "@/model/domain-edit.model";
|
||||
import { SuccessActionResult } from "@/model/server-action-error-return.model";
|
||||
import appService from "@/server/services/app.service";
|
||||
import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
const actionAppDomainEditZodModel = appDomainEditZodModel.merge(z.object({
|
||||
appId: z.string(),
|
||||
id: z.string().nullish()
|
||||
}));
|
||||
|
||||
export const saveEnvVariables = async (prevState: any, inputData: AppEnvVariablesModel, appId: string) =>
|
||||
saveFormAction(inputData, appEnvVariablesZodModel, async (validatedData) => {
|
||||
export const saveDomain = async (prevState: any, inputData: z.infer<typeof actionAppDomainEditZodModel>) =>
|
||||
saveFormAction(inputData, actionAppDomainEditZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const existingApp = await appService.getById(appId);
|
||||
await appService.save({
|
||||
...existingApp,
|
||||
await appService.saveDomain({
|
||||
...validatedData,
|
||||
id: appId,
|
||||
id: validatedData.id ?? undefined
|
||||
});
|
||||
});
|
||||
|
||||
export const deleteDomain = async (domainId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.deleteDomainById(domainId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted domain');
|
||||
});
|
||||
});
|
||||
117
src/app/project/app/[tabName]/domains/domain-edit.-overlay.tsx
Normal file
117
src/app/project/app/[tabName]/domains/domain-edit.-overlay.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useFormState } from 'react-dom'
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormUtils } from "@/lib/form.utilts";
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
import { AppDomain } from "@prisma/client"
|
||||
import { AppDomainEditModel, appDomainEditZodModel } from "@/model/domain-edit.model"
|
||||
import { ServerActionResult } from "@/model/server-action-error-return.model"
|
||||
import { saveDomain } from "./actions"
|
||||
import { toast } from "sonner"
|
||||
import CheckboxFormField from "@/components/custom/checkbox-form-field"
|
||||
|
||||
|
||||
|
||||
export default function DialogEditDialog({ children, domain, appId }: { children: React.ReactNode; domain?: AppDomain; appId: string; }) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
|
||||
const form = useForm<AppDomainEditModel>({
|
||||
resolver: zodResolver(appDomainEditZodModel),
|
||||
defaultValues: {
|
||||
...domain,
|
||||
useSsl: domain?.useSsl === false ? false : true
|
||||
}
|
||||
});
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppDomainEditModel) =>
|
||||
saveDomain(state, {
|
||||
...payload,
|
||||
appId,
|
||||
id: domain?.id
|
||||
}), FormUtils.getInitialFormState<typeof appDomainEditZodModel>());
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('Domain saved successfully');
|
||||
setIsOpen(false);
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof appDomainEditZodModel>(state, form);
|
||||
}, [state]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setIsOpen(true)}>
|
||||
{children}
|
||||
</div>
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(false)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Domain</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your custom domain for this application. Note that the domain must be pointing to the server IP address.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hostname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hostname</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="ex. 80" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxFormField form={form} name="useSsl" label="use HTTPS" />
|
||||
<p className="text-red-500">{state.message}</p>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form >
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { FormUtils } from "@/lib/form.utilts";
|
||||
import { AppSourceInfoInputModel, appSourceInfoInputZodModel } from "@/model/app-source-info.model";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { saveEnvVariables } from "./actions";
|
||||
import { useFormState } from "react-dom";
|
||||
import { ServerActionResult } from "@/model/server-action-error-return.model";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -22,7 +21,10 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { AppExtendedModel } from "@/model/app-extended.model";
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckIcon, CrossIcon } from "lucide-react";
|
||||
import { CheckIcon, CrossIcon, DeleteIcon, EditIcon, TrashIcon, XIcon } from "lucide-react";
|
||||
import DialogEditDialog from "./domain-edit.-overlay";
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { deleteDomain } from "./actions";
|
||||
|
||||
|
||||
export default function DomainsList({ app }: {
|
||||
@@ -42,7 +44,7 @@ export default function DomainsList({ app }: {
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Port</TableHead>
|
||||
<TableHead>SSL</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
<TableHead className="w-[100px]">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -50,15 +52,24 @@ export default function DomainsList({ app }: {
|
||||
<TableRow key={domain.hostname}>
|
||||
<TableCell className="font-medium">{domain.hostname}</TableCell>
|
||||
<TableCell className="font-medium">{domain.port}</TableCell>
|
||||
<TableCell className="font-medium">{domain.useSsl ? <CheckIcon /> : <CrossIcon />}</TableCell>
|
||||
<TableCell className="font-medium"><Button variant="ghost">Edit</Button></TableCell>
|
||||
<TableCell className="font-medium">{domain.useSsl ? <CheckIcon /> : <XIcon />}</TableCell>
|
||||
<TableCell className="font-medium flex gap-2">
|
||||
<DialogEditDialog appId={app.id} domain={domain}>
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</DialogEditDialog>
|
||||
<Button variant="ghost" onClick={() => Toast.fromAction(() => deleteDomain(domain.id))}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Add Domain</Button>
|
||||
<DialogEditDialog appId={app.id}>
|
||||
<Button>Add Domain</Button>
|
||||
</DialogEditDialog>
|
||||
</CardFooter>
|
||||
</Card >
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function EnvEdit({ app }: {
|
||||
FormUtils.mapValidationErrorsToForm<typeof appEnvVariablesZodModel>(state, form);
|
||||
}, [state]);
|
||||
|
||||
const sourceTypeField = form.watch();
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { z } from "zod";
|
||||
import { z, ZodObject, ZodRawShape } from "zod";
|
||||
|
||||
export class ZodUtils {
|
||||
static getFieldNamesAndTypes<TObjectType extends ZodRawShape>(schema: ZodObject<TObjectType>): Map<string, string> {
|
||||
const shape = schema.shape;
|
||||
const fieldMap = new Map<string, string>();
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
fieldMap.set(key, value._def.typeName);
|
||||
}
|
||||
|
||||
return fieldMap;
|
||||
}
|
||||
}
|
||||
|
||||
export const stringToNumber = z.union([z.string(), z.number()])
|
||||
.transform((val) => {
|
||||
@@ -58,22 +71,37 @@ export const stringToDate = z.union([z.string(), z.date()])
|
||||
message: 'Der Eingegebene Wert muss ein Datum sein.',
|
||||
});
|
||||
|
||||
export const stringToOptionalBoolean = z.preprocess((val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
if (val === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (val === 'false') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return val;
|
||||
}, z.boolean().nullish());
|
||||
|
||||
/*z.union([z.string(), z.number(), z.null(), z.undefined()])
|
||||
.transform((val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return null;
|
||||
export const stringToBoolean = z.preprocess((val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
if (val === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
const parsed = parseFloat(val);
|
||||
if (isNaN(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
if (val === 'false') {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (typeof val === 'boolean') {
|
||||
return val;
|
||||
})
|
||||
.refine((val) => val === null || typeof val === 'number', {
|
||||
message: 'Der Eingegebene Wert muss eine Zahl oder leer sein.',
|
||||
});
|
||||
*/
|
||||
}
|
||||
return false; // default false
|
||||
}, z.boolean());
|
||||
|
||||
10
src/model/domain-edit.model.ts
Normal file
10
src/model/domain-edit.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { stringToBoolean, stringToNumber } from "@/lib/zod.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const appDomainEditZodModel = z.object({
|
||||
hostname: z.string().trim().min(1),
|
||||
useSsl: stringToBoolean,
|
||||
port: stringToNumber,
|
||||
})
|
||||
|
||||
export type AppDomainEditModel = z.infer<typeof appDomainEditZodModel>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteUser, RelatedUserModel } from "./index"
|
||||
|
||||
export const AccountModel = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteProject, RelatedProjectModel, CompleteAppDomain, RelatedAppDomainModel, CompleteAppVolume, RelatedAppVolumeModel } from "./index"
|
||||
|
||||
export const AppModel = z.object({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteApp, RelatedAppModel } from "./index"
|
||||
|
||||
export const AppDomainModel = z.object({
|
||||
id: z.string(),
|
||||
hostname: z.string(),
|
||||
port: z.number().int(),
|
||||
useSsl: z.boolean(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteApp, RelatedAppModel } from "./index"
|
||||
|
||||
export const AppVolumeModel = z.object({
|
||||
id: z.string(),
|
||||
containerMountPath: z.string(),
|
||||
appId: z.string(),
|
||||
createdAt: z.date(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteUser, RelatedUserModel } from "./index"
|
||||
|
||||
export const AuthenticatorModel = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteApp, RelatedAppModel } from "./index"
|
||||
|
||||
export const ProjectModel = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteUser, RelatedUserModel } from "./index"
|
||||
|
||||
export const SessionModel = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
import { CompleteAccount, RelatedAccountModel, CompleteSession, RelatedSessionModel, CompleteAuthenticator, RelatedAuthenticatorModel } from "./index"
|
||||
|
||||
export const UserModel = z.object({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../../../prisma/null"
|
||||
|
||||
|
||||
export const VerificationTokenModel = z.object({
|
||||
identifier: z.string(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { App, Prisma, Project } from "@prisma/client";
|
||||
import { App, AppDomain, Prisma } from "@prisma/client";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { AppExtendedModel } from "@/model/app-extended.model";
|
||||
import { ServiceException } from "@/model/service.exception.model";
|
||||
|
||||
class AppService {
|
||||
|
||||
@@ -18,6 +19,7 @@ class AppService {
|
||||
}
|
||||
});
|
||||
revalidateTag(Tags.apps(existingItem.projectId));
|
||||
revalidateTag(Tags.app(existingItem.id));
|
||||
}
|
||||
|
||||
async getAllAppsByProjectID(projectId: string) {
|
||||
@@ -31,8 +33,8 @@ class AppService {
|
||||
})(projectId as string);
|
||||
}
|
||||
|
||||
async getExtendedById(id: string): Promise<AppExtendedModel> {
|
||||
return dataAccess.client.app.findFirstOrThrow({
|
||||
async getExtendedById(appId: string): Promise<AppExtendedModel> {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
}, include: {
|
||||
@@ -40,35 +42,99 @@ class AppService {
|
||||
appDomains: true,
|
||||
appVolumes: true,
|
||||
}
|
||||
});
|
||||
}),
|
||||
[Tags.app(appId)], {
|
||||
tags: [Tags.app(appId)]
|
||||
})(appId);
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return dataAccess.client.app.findFirstOrThrow({
|
||||
async getById(appId: string) {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}),
|
||||
[Tags.app(appId)], {
|
||||
tags: [Tags.app(appId)]
|
||||
})(appId);
|
||||
}
|
||||
|
||||
async save(item: Prisma.AppUncheckedCreateInput | Prisma.AppUncheckedUpdateInput) {
|
||||
let savedItem: Prisma.Prisma__AppClient<App, never, DefaultArgs>;
|
||||
if (item.id) {
|
||||
savedItem = dataAccess.client.app.update({
|
||||
where: {
|
||||
id: item.id as string
|
||||
},
|
||||
data: item
|
||||
});
|
||||
} else {
|
||||
savedItem = dataAccess.client.app.create({
|
||||
data: item as Prisma.AppUncheckedCreateInput
|
||||
});
|
||||
try {
|
||||
if (item.id) {
|
||||
savedItem = dataAccess.client.app.update({
|
||||
where: {
|
||||
id: item.id as string
|
||||
},
|
||||
data: item
|
||||
});
|
||||
} else {
|
||||
savedItem = dataAccess.client.app.create({
|
||||
data: item as Prisma.AppUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
revalidateTag(Tags.apps(item.projectId as string));
|
||||
revalidateTag(Tags.app(item.id as string));
|
||||
}
|
||||
|
||||
revalidateTag(Tags.apps(item.projectId as string));
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
async saveDomain(domainToBeSaved: Prisma.AppDomainUncheckedCreateInput | Prisma.AppDomainUncheckedUpdateInput) {
|
||||
let savedItem: AppDomain;
|
||||
const existingApp = await this.getExtendedById(domainToBeSaved.appId as string);
|
||||
const existingDomainWithSameHostname = await dataAccess.client.appDomain.findFirst({
|
||||
where: {
|
||||
hostname: domainToBeSaved.hostname as string,
|
||||
}
|
||||
});
|
||||
if (domainToBeSaved.id && domainToBeSaved.id !== existingDomainWithSameHostname?.id) {
|
||||
throw new ServiceException("Hostname is already in use by this or another app.");
|
||||
}
|
||||
try {
|
||||
if (domainToBeSaved.id) {
|
||||
savedItem = await dataAccess.client.appDomain.update({
|
||||
where: {
|
||||
id: domainToBeSaved.id as string
|
||||
},
|
||||
data: domainToBeSaved
|
||||
});
|
||||
} else {
|
||||
savedItem = await dataAccess.client.appDomain.create({
|
||||
data: domainToBeSaved as Prisma.AppDomainUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
revalidateTag(Tags.apps(existingApp.projectId as string));
|
||||
revalidateTag(Tags.app(existingApp.id as string));
|
||||
}
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
async deleteDomainById(id: string) {
|
||||
const existingDomain = await dataAccess.client.appDomain.findFirst({
|
||||
where: {
|
||||
id
|
||||
}, include: {
|
||||
app: true
|
||||
}
|
||||
});
|
||||
if (!existingDomain) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await dataAccess.client.appDomain.delete({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
revalidateTag(Tags.app(existingDomain.appId));
|
||||
revalidateTag(Tags.apps(existingDomain.app.projectId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appService = new AppService();
|
||||
|
||||
@@ -11,4 +11,8 @@ export class Tags {
|
||||
static apps(projectId: string) {
|
||||
return `apps-${projectId}`;
|
||||
}
|
||||
|
||||
static app(appId: string) {
|
||||
return `app-${appId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user