add call to validate database endpoint if database requires it

This commit is contained in:
Jacob Overgaard
2022-08-24 11:00:23 +02:00
parent bdf4b31df6
commit 0db991ccb8
6 changed files with 137 additions and 80 deletions

View File

@@ -46,7 +46,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/InstallValidateDatabaseRequest'
$ref: '#/components/schemas/InstallSetupDatabaseConfiguration'
required: true
responses:
'201':
@@ -341,13 +341,6 @@ components:
required:
- user
- telemetryLevel
InstallValidateDatabaseRequest:
type: object
properties:
database:
$ref: '#/components/schemas/InstallSetupDatabaseConfiguration'
required:
- database
ServerStatus:
type: string
enum:

View File

@@ -102,9 +102,6 @@ export interface components {
telemetryLevel: components["schemas"]["ConsentLevel"];
database?: components["schemas"]["InstallSetupDatabaseConfiguration"];
};
InstallValidateDatabaseRequest: {
database: components["schemas"]["InstallSetupDatabaseConfiguration"];
};
/** @enum {string} */
ServerStatus: "running" | "must-install" | "must-upgrade";
StatusResponse: {
@@ -184,7 +181,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["InstallValidateDatabaseRequest"];
"application/json": components["schemas"]["InstallSetupDatabaseConfiguration"];
};
};
};

View File

@@ -1,8 +1,9 @@
import { UUIButtonElement } from '@umbraco-ui/uui';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { postInstallSetup, postInstallValidateDatabase } from '../core/api/fetcher';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models';
import { UmbInstallerContext } from './installer-context';
@@ -69,6 +70,11 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
margin-left: auto;
min-width: 120px;
}
.error {
color: var(--uui-color-danger);
padding: var(--uui-size-space-2) 0;
}
`,
];
@@ -90,6 +96,9 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
@state()
private _installerStore?: UmbInstallerContext;
@state()
private _validationErrorMessage = '';
private storeDataSubscription?: Subscription;
private storeSettingsSubscription?: Subscription;
@@ -136,15 +145,19 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
this._installerStore?.appendData({ database });
}
private _handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
private _handleSubmit = async (evt: SubmitEvent) => {
evt.preventDefault();
const form = e.target as HTMLFormElement;
const form = evt.target as HTMLFormElement;
if (!form) return;
const isValid = form.checkValidity();
if (!isValid) return;
if (!this._installerStore) return;
this._installButton.state = 'waiting';
// Only append the database if it's not pre-configured
if (!this._preConfiguredDatabase) {
const formData = new FormData(form);
@@ -154,6 +167,39 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
const server = formData.get('server') as string;
const name = formData.get('name') as string;
const useIntegratedAuthentication = formData.has('useIntegratedAuthentication');
const connectionString = formData.get('connectionString') as string;
// Validate connection
const selectedDatabase = this._databases.find((x) => x.id === id);
if (selectedDatabase?.requiresConnectionTest) {
try {
let databaseDetails: UmbracoPerformInstallDatabaseConfiguration = {};
if (connectionString) {
databaseDetails.connectionString = connectionString;
} else {
databaseDetails = {
id,
username,
password,
server,
useIntegratedAuthentication,
name,
};
}
await postInstallValidateDatabase(databaseDetails);
} catch (e) {
if (e instanceof postInstallSetup.Error) {
const error = e.getActualType();
console.warn('Database validation failed', error.data);
this._validationErrorMessage = error.data.detail ?? 'Could not verify database connection';
} else {
this._validationErrorMessage = 'A server error happened when trying to validate the database';
}
this._installButton.state = 'failed';
return;
}
}
const database = {
...this._installerStore?.getData().database,
@@ -163,14 +209,13 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
server,
name,
useIntegratedAuthentication,
connectionString,
} as UmbracoPerformInstallDatabaseConfiguration;
this._installerStore?.appendData({ database });
}
this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true }));
this._installButton.state = 'waiting';
};
private _onBack() {
@@ -321,6 +366,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
${this._preConfiguredDatabase
? this._renderPreConfiguredDatabase(this._preConfiguredDatabase)
: this._renderDatabaseSelection()}
${this._validationErrorMessage ? html` <div class="error">${this._validationErrorMessage}</div> ` : nothing}
<div id="buttons">
<uui-button label="Back" @click=${this._onBack} look="secondary"></uui-button>

View File

@@ -48,7 +48,7 @@ export const Step3Database: Story = () => html`<umb-installer-database></umb-ins
Step3Database.storyName = 'Step 3: Database';
Step3Database.parameters = {
actions: {
handles: ['previous', 'next'],
handles: ['previous', 'submit'],
},
};
@@ -56,7 +56,7 @@ export const Step3DatabasePreconfigured: Story = () => html`<umb-installer-datab
Step3DatabasePreconfigured.storyName = 'Step 3: Database (preconfigured)';
Step3DatabasePreconfigured.parameters = {
actions: {
handles: ['previous', 'next'],
handles: ['previous', 'submit'],
},
msw: {
handlers: {

View File

@@ -1,7 +1,12 @@
import { rest } from 'msw';
import umbracoPath from '../../core/helpers/umbraco-path';
import { PostInstallRequest, ProblemDetails, UmbracoInstaller } from '../../core/models';
import {
PostInstallRequest,
ProblemDetails,
UmbracoInstaller,
UmbracoPerformInstallDatabaseConfiguration,
} from '../../core/models';
export const handlers = [
rest.get(umbracoPath('/install/settings'), (_req, res, ctx) => {
@@ -73,6 +78,26 @@ export const handlers = [
);
}),
rest.post(umbracoPath('/install/validateDatabase'), async (req, res, ctx) => {
const body = await req.json<UmbracoPerformInstallDatabaseConfiguration>();
if (body.name === 'validate') {
return res(
ctx.status(400),
ctx.json<ProblemDetails>({
type: 'connection',
status: 400,
detail: 'Database connection failed',
})
);
}
return res(
// Respond with a 200 status code
ctx.status(201)
);
}),
rest.post(umbracoPath('/install/setup'), async (req, res, ctx) => {
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
const body = await req.json<PostInstallRequest>();

View File

@@ -3,103 +3,99 @@ import { body, defaultResponse, endpoint, request, response } from '@airtasker/s
import { ProblemDetails } from './models';
@endpoint({
method: 'GET',
path: '/install/settings',
method: 'GET',
path: '/install/settings',
})
export class GetInstallSettings {
@response({ status: 200 })
success(@body body: InstallSettingsResponse) {}
@response({ status: 200 })
success(@body body: InstallSettingsResponse) {}
@defaultResponse
default(@body body: ProblemDetails) {}
@defaultResponse
default(@body body: ProblemDetails) {}
}
@endpoint({
method: 'POST',
path: '/install/setup',
method: 'POST',
path: '/install/setup',
})
export class PostInstallSetup {
@request
request(@body body: InstallSetupRequest) {}
@request
request(@body body: InstallSetupRequest) {}
@response({ status: 201 })
success() {}
@response({ status: 201 })
success() {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
}
@endpoint({
method: 'POST',
path: '/install/validateDatabase',
method: 'POST',
path: '/install/validateDatabase',
})
export class PostInstallValidateDatabase {
@request
request(@body body: InstallValidateDatabaseRequest) {}
@request
request(@body body: InstallSetupDatabaseConfiguration) {}
@response({ status: 201 })
success() {}
@response({ status: 201 })
success() {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
@response({ status: 400 })
badRequest(@body body: ProblemDetails) {}
}
export interface InstallSetupRequest {
user: InstallSetupUserConfiguration;
telemetryLevel: ConsentLevel;
database?: InstallSetupDatabaseConfiguration;
}
export interface InstallValidateDatabaseRequest {
database: InstallSetupDatabaseConfiguration;
user: InstallSetupUserConfiguration;
telemetryLevel: ConsentLevel;
database?: InstallSetupDatabaseConfiguration;
}
export interface InstallSettingsResponse {
user: InstallUserModel;
databases: InstallDatabaseModel[];
user: InstallUserModel;
databases: InstallDatabaseModel[];
}
export interface InstallUserModel {
minCharLength: number;
minNonAlphaNumericLength: number;
consentLevels: TelemetryModel[];
minCharLength: number;
minNonAlphaNumericLength: number;
consentLevels: TelemetryModel[];
}
export interface InstallSetupUserConfiguration {
name: string;
email: string;
password: string;
subscribeToNewsletter: boolean;
name: string;
email: string;
password: string;
subscribeToNewsletter: boolean;
}
export interface InstallSetupDatabaseConfiguration {
id?: string;
server?: string | null;
password?: string | null;
username?: string | null;
name?: string | null;
providerName?: string | null;
useIntegratedAuthentication?: boolean | null;
connectionString?: string | null;
id?: string;
server?: string | null;
password?: string | null;
username?: string | null;
name?: string | null;
providerName?: string | null;
useIntegratedAuthentication?: boolean | null;
connectionString?: string | null;
}
export interface TelemetryModel {
level: ConsentLevel;
description: string;
level: ConsentLevel;
description: string;
}
export interface InstallDatabaseModel {
id: string;
sortOrder: number;
displayName: string;
defaultDatabaseName: string;
providerName: null | string;
isConfigured: boolean;
requiresServer: boolean;
serverPlaceholder: null | string;
requiresCredentials: boolean;
supportsIntegratedAuthentication: boolean;
requiresConnectionTest: boolean;
id: string;
sortOrder: number;
displayName: string;
defaultDatabaseName: string;
providerName: null | string;
isConfigured: boolean;
requiresServer: boolean;
serverPlaceholder: null | string;
requiresCredentials: boolean;
supportsIntegratedAuthentication: boolean;
requiresConnectionTest: boolean;
}
export type ConsentLevel = 'Minimal' | 'Basic' | 'Detailed';