validation end point implementation

This commit is contained in:
Niels Lyngsø
2024-04-09 15:37:18 +02:00
parent 44ec3be4dc
commit 9ffcc49ee7
17 changed files with 340 additions and 38 deletions

View File

@@ -1,2 +1,4 @@
export * from './validation.context.js';
export * from './validation.context-token.js';
export * from './server-model-validation.context.js';
export * from './server-model-validation.context-token.js';

View File

@@ -0,0 +1,6 @@
import type { UmbServerModelValidationContext } from './server-model-validation.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_SERVER_MODEL_VALIDATION_CONTEXT = new UmbContextToken<UmbServerModelValidationContext>(
'UmbServerModelValidationContext',
);

View File

@@ -0,0 +1,103 @@
import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js';
import type { UmbValidator } from '../interfaces/validator.interface.js';
import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js';
import { UMB_SERVER_MODEL_VALIDATION_CONTEXT } from './server-model-validation.context-token.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
export class UmbServerModelValidationContext
extends UmbContextBase<UmbServerModelValidationContext>
implements UmbValidator
{
#validatePromise?: Promise<boolean>;
#validatePromiseResolve?: (valid: boolean) => void;
#validatePromiseReject?: () => void;
#context?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#isValid = true;
#translators: Array<UmbValidationMessageTranslator> = [];
// Hold server feedback...
#serverFeedback: Record<string, Array<string>> = {};
constructor(host: UmbControllerHost) {
super(host, UMB_SERVER_MODEL_VALIDATION_CONTEXT);
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
if (this.#context) {
this.#context.removeValidator(this);
}
this.#context = context;
context.addValidator(this);
// Run translators?
});
}
async askServerForValidation(requestPromise: Promise<{ data: string | undefined }>): Promise<void> {
this.#context?.messages.removeMessagesByType('server');
this.#validatePromiseReject?.();
this.#validatePromise = new Promise<boolean>((resolve, reject) => {
this.#validatePromiseResolve = resolve;
this.#validatePromiseReject = reject;
});
this.#serverFeedback = {};
// Ask the server for validation...
const { data } = await tryExecuteAndNotify(this, requestPromise);
console.log('VALIDATE — Got server response:');
console.log(data);
this.#validatePromiseResolve?.(true);
this.#validatePromiseResolve = undefined;
this.#validatePromiseReject = undefined;
}
addTranslator(translator: UmbValidationMessageTranslator): void {
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
}
removeTranslator(translator: UmbValidationMessageTranslator): void {
const index = this.#translators.indexOf(translator);
if (index !== -1) {
this.#translators.splice(index, 1);
}
}
get isValid(): boolean {
return this.#isValid;
}
validate(): Promise<boolean> {
// TODO: Return to this decision once we have a bit more implementation to perspectives against: [NL]
// If we dont have a validatePromise, we valid cause then no one has called askServerForValidation(). [NL] (I might change my mind about this one, to then say we are invalid unless we have been validated by the server... )
return this.#validatePromise ?? Promise.resolve(true);
}
reset(): void {}
focusFirstInvalidElement(): void {}
hostConnected(): void {
super.hostConnected();
if (this.#context) {
this.#context.addValidator(this);
}
}
hostDisconnected(): void {
super.hostDisconnected();
if (this.#context) {
this.#context.removeValidator(this);
this.#context = undefined;
}
}
destroy(): void {
// TODO: make sure we destroy things properly:
this.#translators = [];
super.destroy();
}
}

View File

@@ -1,4 +1,3 @@
import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
@@ -12,26 +11,12 @@ export interface UmbValidationMessage {
}
export class UmbValidationMessagesManager {
#translators: Array<UmbValidationMessageTranslator> = [];
#messages = new UmbArrayState<UmbValidationMessage>([], (x) => x.key);
constructor() {
this.#messages.asObservable().subscribe((x) => console.log('messages:', x));
}
addTranslator(translator: UmbValidationMessageTranslator): void {
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
}
removeTranslator(translator: UmbValidationMessageTranslator): void {
const index = this.#translators.indexOf(translator);
if (index !== -1) {
this.#translators.splice(index, 1);
}
}
/*
serializeMessages(fromPath: string, toPath: string): void {
this.#messages.setValue(

View File

@@ -3,3 +3,5 @@ export * from './controllers/index.js';
export * from './events/index.js';
export * from './interfaces/index.js';
export * from './mixins/index.js';
export * from './translators/index.js';
export * from './utils/index.js';

View File

@@ -0,0 +1 @@
export * from './variant-values-validation-message-translator.controller.js';

View File

@@ -0,0 +1,35 @@
import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js';
import { UmbDataPathValueFilter } from '../utils/data-path-value-filter.function.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
export class UmbVariantValuesValidationMessageTranslator
extends UmbControllerBase
implements UmbValidationMessageTranslator
{
//
#workspace: UmbVariantDatasetWorkspaceContext;
constructor(host: UmbControllerHost, workspaceContext: UmbVariantDatasetWorkspaceContext) {
super(host);
this.#workspace = workspaceContext;
}
match(message: string): boolean {
//return message.startsWith('values[');
// regex match, for "values[" and then a number:
return /^values\[\d+\]/.test(message);
}
translate(message: string): string {
/*
// retrieve the number from the message values index:
const index = parseInt(message.substring(7, message.indexOf(']')));
//
this.#workspace.getCurrentData();
// replace the values[ number ] with values [ number + 1 ], continues by the rest of the path:
return 'values[' + UmbDataPathValueFilter() + message.substring(message.indexOf(']'));
*/
return 'not done';
}
}

View File

@@ -3,17 +3,17 @@ import type { UmbVariantableValueModel } from '@umbraco-cms/backoffice/models';
/**
* write a JSON-Path filter similar to `?(@.alias = 'myAlias' && @.culture == 'en-us' && @.segment == 'mySegment')`
* where culture and segment are optional
* @param property
* @param value
* @returns
*/
export function UmbDataPathValueFilter(property: Partial<UmbVariantableValueModel>): string {
export function UmbDataPathValueFilter(value: Omit<UmbVariantableValueModel, 'value'>): string {
// write a array of strings for each property, where alias must be present and culture and segment are optional
const filters: Array<string> = [`@.alias = '${property.alias}'`];
if (property.culture) {
filters.push(`@.culture == '${property.culture}'`);
const filters: Array<string> = [`@.alias = '${value.alias}'`];
if (value.culture) {
filters.push(`@.culture == '${value.culture}'`);
}
if (property.segment) {
filters.push(`@.segment == '${property.segment}'`);
if (value.segment) {
filters.push(`@.segment == '${value.segment}'`);
}
return `?(${filters.join(' && ')})`;
}

View File

@@ -0,0 +1 @@
export * from './data-path-value-filter.function.js';

View File

@@ -3,6 +3,7 @@ import { html, customElement, state, ifDefined, repeat } from '@umbraco-cms/back
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbDataPathValueFilter } from '@umbraco-cms/backoffice/validation';
/**
* @element umb-property-editor-config
@@ -43,10 +44,10 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement {
? repeat(
this._properties,
(property) => property.alias,
(property, index) =>
(property) =>
// TODO: Make a helper method to generate data-path entry for a property.
html`<umb-property
.dataPath="values[?(@.alias = '${property.alias}'}]"
.dataPath="values[${UmbDataPathValueFilter(property)}]"
label=${property.label}
description=${ifDefined(property.description)}
alias=${property.alias}

View File

@@ -3,7 +3,6 @@ import { UmbDocumentServerDataSource } from './document-detail.server.data-sourc
import { UMB_DOCUMENT_DETAIL_STORE_CONTEXT } from './document-detail.store.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbDocumentDetailRepository extends UmbDetailRepositoryBase<UmbDocumentDetailModel> {
constructor(host: UmbControllerHost) {
super(host, UmbDocumentServerDataSource, UMB_DOCUMENT_DETAIL_STORE_CONTEXT);

View File

@@ -128,7 +128,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
/**
* Inserts a new Document on the server
* @param {UmbDocumentDetailModel} model
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
* @memberof UmbDocumentServerDataSource
*/
@@ -162,7 +162,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
/**
* Updates a Document on the server
* @param {UmbDocumentDetailModel} Document
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
* @memberof UmbDocumentServerDataSource
*/

View File

@@ -0,0 +1,46 @@
import type { UmbDocumentDetailModel } from '../../types.js';
import { UmbDocumentValidationServerDataSource } from './document-validation.server.data-source.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
type DetailModelType = UmbDocumentDetailModel;
export class UmbDocumentValidationRepository extends UmbRepositoryBase {
#validationDataSource: UmbDocumentValidationServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#validationDataSource = new UmbDocumentValidationServerDataSource(this);
}
/**
* Returns a promise with an observable of the detail for the given unique
* @param {DetailModelType} model
* @param {string | null} [parentUnique=null]
* @return {*}
* @memberof UmbDetailRepositoryBase
*/
async validateCreate(model: DetailModelType, parentUnique: string | null) {
if (!model) throw new Error('Data is missing');
const { data, error } = await this.#validationDataSource.validateCreate(model, parentUnique);
return { data, error };
}
/**
* Saves the given data
* @param {DetailModelType} model
* @return {*}
* @memberof UmbDetailRepositoryBase
*/
async validateSave(model: DetailModelType) {
if (!model) throw new Error('Data is missing');
if (!model.unique) throw new Error('Unique is missing');
const { data, error } = await this.#validationDataSource.validateUpdate(model);
return { data, error };
}
}

View File

@@ -0,0 +1,90 @@
import type { UmbDocumentDetailModel } from '../../types.js';
import {
type CreateDocumentRequestModel,
DocumentResource,
type UpdateDocumentRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A server data source for Document Validation
* @export
* @class UmbDocumentPublishingServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class UmbDocumentValidationServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentPublishingServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentPublishingServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Validate a new Document on the server
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
*/
async validateCreate(model: UmbDocumentDetailModel, parentUnique: string | null = null) {
if (!model) throw new Error('Document is missing');
if (!model.unique) throw new Error('Document unique is missing');
// TODO: make data mapper to prevent errors
const requestBody: CreateDocumentRequestModel = {
id: model.unique,
parent: parentUnique ? { id: parentUnique } : null,
documentType: { id: model.documentType.unique },
template: model.template ? { id: model.template.unique } : null,
values: model.values,
variants: model.variants,
};
const { data, error } = await tryExecuteAndNotify(
this.#host,
DocumentResource.postDocumentValidate({
requestBody,
}),
);
if (data) {
return { data };
}
return { error };
}
/**
* Validate a existing Document
* @param {UmbDocumentDetailModel} model - Document Model
* @return {*}
*/
async validateUpdate(model: UmbDocumentDetailModel) {
if (!model.unique) throw new Error('Unique is missing');
// TODO: make data mapper to prevent errors
const requestBody: UpdateDocumentRequestModel = {
template: model.template ? { id: model.template.unique } : null,
values: model.values,
variants: model.variants,
};
const { data, error } = await tryExecuteAndNotify(
this.#host,
DocumentResource.putDocumentByIdValidate({
id: model.unique,
requestBody,
}),
);
if (!error) {
return { data };
}
return { error };
}
}

View File

@@ -0,0 +1,2 @@
export { UmbDocumentValidationRepository } from './document-validation.repository.js';
export { UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,13 @@
import { UmbDocumentValidationRepository } from './document-validation.repository.js';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS = 'Umb.Repository.Document.Validation';
const validationRepository: ManifestRepository = {
type: 'repository',
alias: UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS,
name: 'Document Validation Repository',
api: UmbDocumentValidationRepository,
};
export const manifests = [validationRepository];

View File

@@ -17,6 +17,7 @@ import {
} from '../modals/index.js';
import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js';
import { UmbUnpublishDocumentEntityAction } from '../entity-actions/unpublish.action.js';
import { UmbDocumentValidationRepository } from '../repository/validation/document-validation.repository.js';
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js';
import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
@@ -47,6 +48,7 @@ import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type';
import { UmbServerModelValidationContext } from '@umbraco-cms/backoffice/validation';
type EntityType = UmbDocumentDetailModel;
export class UmbDocumentWorkspaceContext
@@ -74,6 +76,9 @@ export class UmbDocumentWorkspaceContext
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
public readonly languages = this.#languages.asObservable();
#serverValidation = new UmbServerModelValidationContext(this);
#validationRepository?: UmbDocumentValidationRepository;
public isLoaded() {
return this.#getDataPromise;
}
@@ -473,7 +478,7 @@ export class UmbDocumentWorkspaceContext
if (variantIdsToParseForValues.some((x) => x.compare(value))) {
return value;
} else {
// If not we will find the value in the persisted data and use that instead.
// If not, then we will find the value in the persisted data and use that instead.
return persistedData?.values.find(
(x) => x.alias === value.alias && x.culture === value.culture && x.segment === value.segment,
);
@@ -494,9 +499,7 @@ export class UmbDocumentWorkspaceContext
};
}
async #performSaveOrCreate(selectedVariants: Array<UmbVariantId>): Promise<void> {
const saveData = this.#buildSaveData(selectedVariants);
async #performSaveOrCreate(saveData: UmbDocumentDetailModel): Promise<void> {
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
@@ -528,13 +531,13 @@ export class UmbDocumentWorkspaceContext
this.#persistedData.setValue(data);
this.#currentData.setValue(data);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.getUnique()!,
entityType: this.getEntityType(),
});
actionEventContext.dispatchEvent(event);
eventContext.dispatchEvent(event);
}
}
@@ -570,24 +573,36 @@ export class UmbDocumentWorkspaceContext
variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
}
const saveData = this.#buildSaveData(variantIds);
this.#validationRepository ??= new UmbDocumentValidationRepository(this);
if (this.getIsNew()) {
const parent = this.#parent.getValue();
if (!parent) throw new Error('Parent is not set');
this.#serverValidation.askServerForValidation(this.#validationRepository.validateCreate(saveData, parent.unique));
} else {
this.#serverValidation.askServerForValidation(this.#validationRepository.validateSave(saveData));
}
// TODO: Only validate the specified selection.. [NL]
return this.validateAndSubmit(
async () => {
return this.#performSaveAndPublish(variantIds);
return this.#performSaveAndPublish(variantIds, saveData);
},
async () => {
// If data of the selection is not valid Then just save:
await this.#performSaveOrCreate(variantIds);
await this.#performSaveOrCreate(saveData);
// Reject even thought the save was successful, but we did not publish, which is what we want to symbolize here. [NL]
return await Promise.reject();
},
);
}
async #performSaveAndPublish(variantIds: Array<UmbVariantId>): Promise<void> {
async #performSaveAndPublish(variantIds: Array<UmbVariantId>, saveData: UmbDocumentDetailModel): Promise<void> {
const unique = this.getUnique();
if (!unique) throw new Error('Unique is missing');
await this.#performSaveOrCreate(variantIds);
await this.#performSaveOrCreate(saveData);
await this.publishingRepository.publish(
unique,
@@ -624,7 +639,8 @@ export class UmbDocumentWorkspaceContext
variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
}
return await this.#performSaveOrCreate(variantIds);
const saveData = this.#buildSaveData(variantIds);
return await this.#performSaveOrCreate(saveData);
}
public requestSubmit() {