wip: trpc

This commit is contained in:
Andras Bacsai
2022-12-21 13:06:44 +01:00
parent 4fa0f2d04a
commit 9c6f412f04
26 changed files with 3383 additions and 39 deletions

View File

@@ -1,11 +1,8 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { LayoutLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import Cookies from 'js-cookie';
export const ssr = false;
export const load: LayoutLoad = async ({ url }) => {
export const load = async () => {
try {
return await trpc.dashboard.resources.query();
} catch (err) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import type { PageParentData } from './$types';
export let data: PageParentData;
const application = data.application.data;
const settings = data.settings.data;
import { page } from '$app/stores';
const { id } = $page.params;
import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
isDeploymentEnabled,
trpc
} from '$lib/store';
import { errorNotification } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) {
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
if ($status.application.isRunning) return;
isBot = !isBot;
application.settings.isBot = isBot;
application.fqdn = null;
setLocation(application, settings);
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
try {
await trpc.applications.saveSettings.mutate({
id,
previews,
dualCerts,
isBot,
autodeploy,
isDBBranching
});
return addToast({
message: 'Settings saved',
type: 'success'
});
} catch (error) {
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
return errorNotification(error);
} finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
}
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Features</div>
</div>
<div class="px-4 lg:pb-10 pb-6">
{#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="autodeploy"
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
/>
</div>
{#if !application.settings.isBot && !application.simpleDockerfile}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Enable preview deployments from pull or merge requests."
/>
</div>
{/if}
{:else}
No features available for this application
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let secrets = data.secrets;
let previewSecrets = data.previewSecrets;
const application = data.application.data;
import pLimit from 'p-limit';
import { page } from '$app/stores';
import { addToast, trpc } from '$lib/store';
import Secret from './_components/Secret.svelte';
import PreviewSecret from './_components/PreviewSecret.svelte';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';
const limit = pLimit(1);
const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() {
const { data } = await trpc.applications.getSecrets.query({ id });
previewSecrets = [...data.previewSecrets];
secrets = [...data.secrets];
}
async function getValues() {
if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
return {
name: name.trim(),
value: value.trim(),
createSecret: !secrets.find((secret: any) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, createSecret }) =>
limit(async () => {
try {
if (!name || !value) return;
if (createSecret) {
await trpc.applications.newSecret.mutate({
id,
name,
value
});
addToast({
message: 'Secret created.',
type: 'success'
});
} else {
await trpc.applications.updateSecret.mutate({
id,
name,
value
});
addToast({
message: 'Secret updated.',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
})
)
);
batchSecrets = '';
await refreshSecrets();
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Secrets</div>
</div>
{#each secrets as secret, index}
{#key secret.id}
<Secret
{index}
length={secrets.length}
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
{/key}
{/each}
<div class="lg:pt-0 pt-10">
<Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
</div>
{#if !application.settings.isBot && !application.simpleDockerfile}
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 pt-8">
Preview Secrets <Explainer
explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
/>
</div>
</div>
{#if previewSecrets.length !== 0}
{#each previewSecrets as secret, index}
{#key index}
<PreviewSecret
{index}
length={secrets.length}
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
{/key}
{/each}
{:else}
Add secrets first to see Preview Secrets.
{/if}
{/if}
</div>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="flex flex-row space-x-2">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div>
<textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full"
/>
</form>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const { data } = await trpc.applications.getSecrets.query({ id });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,131 @@
<script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = '';
export let value = '';
export let isBuildSecret = false;
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast, trpc } from '$lib/store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function updatePreviewSecret() {
try {
await trpc.applications.updateSecret.mutate({
id,
name: name.trim(),
value: value.trim(),
isPreview: true
});
addToast({
message: 'Secret updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
id="secretName"
readonly
disabled
value={name}
required
placeholder="EXAMPLE_VARIABLE"
class=" w-full"
/>
</div>
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
id="secretValue"
name="secretValue"
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
/>
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
aria-pressed="false"
class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
>
<span class="sr-only">Is build secret?</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if index === 0 || length === 0}
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,193 @@
<script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = '';
export let value = '';
export let isBuildSecret = false;
export let isNewSecret = false;
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast, trpc } from '$lib/store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
function cleanupState() {
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
}
async function removeSecret() {
try {
await trpc.applications.deleteSecret.mutate({ id, name });
cleanupState();
addToast({
message: 'Secret removed.',
type: 'success'
});
dispatch('refresh');
} catch (error) {
return errorNotification(error);
}
}
async function addNewSecret() {
try {
if (!name.trim()) return errorNotification({ message: 'Name is required.' });
if (!value.trim()) return errorNotification({ message: 'Value is required.' });
await trpc.applications.newSecret.mutate({
id,
name: name.trim(),
value: value.trim(),
isBuildSecret
});
cleanupState();
addToast({
message: 'Secret added.',
type: 'success'
});
dispatch('refresh');
} catch (error) {
return errorNotification(error);
}
}
async function updateSecret({
changeIsBuildSecret = false
}: { changeIsBuildSecret?: boolean } = {}) {
if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
if (isNewSecret) return;
try {
await trpc.applications.updateSecret.mutate({
id,
name: name.trim(),
value: value.trim(),
isBuildSecret,
isPreview: false
});
addToast({
message: 'Secret updated.',
type: 'success'
});
dispatch('refresh');
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
class:border-coolgray-300={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/>
</div>
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
/>
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
on:click={() => updateSecret({ changeIsBuildSecret: true })}
aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
>
<span class="sr-only">Is build secret?</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
</div>
</div>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const application = data.application.data;
let persistentStorages = data.persistentStorages;
import { page } from '$app/stores';
import Storage from './components/Storage.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { trpc } from '$lib/store';
let composeJson: any = JSON.parse(application?.dockerComposeFile || '{}');
let predefinedVolumes: any[] = [];
if (composeJson?.services) {
for (const [_, service] of Object.entries(composeJson.services)) {
if (service?.volumes) {
for (const [_, volumeName] of Object.entries(service.volumes)) {
let [volume, target] = volumeName.split(':');
if (volume === '.') {
volume = target;
}
if (!target) {
target = volume;
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
predefinedVolumes.push({ id: volume, path: target, predefined: true });
}
}
}
}
const { id } = $page.params;
async function refreshStorage() {
const { data } = await trpc.applications.getStorages.query({ id });
persistentStorages = [...data.persistentStorages];
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Persistent Volumes</div>
</div>
{#if predefinedVolumes.length > 0}
<div class="title">Predefined Volumes</div>
<div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
<div class="font-bold uppercase">Volume Id</div>
<div class="font-bold uppercase">Mount Dir</div>
</div>
</div>
<div class="gap-4">
{#each predefinedVolumes as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
</div>
{/if}
{#if persistentStorages.length > 0}
<div class="title" class:pt-10={predefinedVolumes.length > 0}>Custom Volumes</div>
{/if}
{#each persistentStorages as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
<div class="Preview Secrets" class:pt-10={predefinedVolumes.length > 0}>
Add New Volume <Explainer
position="dropdown-bottom"
explanation="You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
/>
</div>
<Storage on:refresh={refreshStorage} isNew />
</div>
</div>

View File

@@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const { data } = await trpc.applications.getStorages.query({ id });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@@ -0,0 +1,114 @@
<script lang="ts">
export let isNew = false;
export let storage: any = {
id: null,
path: null
};
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/common';
import { addToast, trpc } from '$lib/store';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required');
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
await trpc.applications.updateStorage.mutate({
id,
path: storage.path,
storageId: storage.id,
newStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (newStorage) {
addToast({
message: 'Storage created',
type: 'success'
});
} else {
addToast({
message: 'Storage updated',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
}
async function removeStorage() {
try {
await trpc.applications.deleteStorage.mutate({
id,
path: storage.path
});
dispatch('refresh');
addToast({
message: 'Storage removed',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full lg:px-0 px-4">
{#if storage.predefined}
<div class="flex flex-col lg:flex-row gap-4 pb-2">
<input disabled readonly class="w-full" value={storage.id} />
<input disabled readonly class="w-full" bind:value={storage.path} />
</div>
{:else}
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId}
{#if storage.oldPath}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/>
{:else}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
/>
{/if}
{/if}
<input
disabled={!isNew}
readonly={!isNew}
class="w-full"
bind:value={storage.path}
required
placeholder="eg: /data"
/>
<div class="flex items-center justify-center">
{#if isNew}
<div class="w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
>Add</button
>
</div>
{:else}
<div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
</div>
{/if}
</div>
</div>
{/if}
</div>

View File

@@ -2,6 +2,60 @@ import { goto } from '$app/navigation';
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
export async function saveForm() {
return await trpc.applications.save.mutate();
export async function saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration) {
let {
name,
buildPack,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
gitCommitHash,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
simpleDockerfile,
dockerRegistryImageName
} = application;
return await trpc.applications.save.mutate({
id,
name,
buildPack,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
gitCommitHash,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
simpleDockerfile,
dockerRegistryImageName,
baseDatabaseBranch,
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
});
}