@eclipse-emfcloud/modelserver-client
Version:
Typescript rest client to interact with an EMF.cloud modelserver
243 lines (221 loc) • 9.63 kB
text/typescript
/********************************************************************************
* Copyright (c) 2021-2022 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0, or the MIT License which is
* available at https://opensource.org/licenses/MIT.
*
* SPDX-License-Identifier: EPL-2.0 OR MIT
*******************************************************************************/
import * as jsonpatch from 'fast-json-patch';
import { deepClone, Operation } from 'fast-json-patch';
import URI from 'urijs';
import { ModelPatch, ModelUpdateResult } from './model-server-client-api-v2';
import { ModelServerElement } from './model/base-model';
import { Operations } from './utils/patch-utils';
import * as Type from './utils/type-util';
/**
* A `ModelServerMessage` represents the data payload that is sent by the modelserver
* when responding to incoming requests. It's also used by the client to send messages (e.g. keepAlive messages)
* to the server.
* @typeParam D Concrete type of the `data` property. Default is `unknown`.
*/
export interface ModelServerMessage<D = unknown> {
/** The message data */
data: D;
/** The message type. Is a literal of {@link MessageType} unless the modelserver has been extended with custom types */
type: string;
}
export namespace ModelServerMessage {
/**
* Guard guard to check wether a given object is of type {@link ModelServerMessage}.
* @param object The object to check.
* @returns The given object as {@link ModelServerMessage} or `false`.
*/
export function is(object: unknown): object is ModelServerMessage {
return Type.AnyObject.is(object) && Type.isString(object, 'type') && (object as any).data !== undefined;
}
}
/**
* Enumeration of the default types of a {@link ModelServerMessage}.
*/
// eslint-disable-next-line no-shadow
export enum MessageType {
success = 'success',
warning = 'warning',
error = 'error',
open = 'open',
close = 'close',
fullUpdate = 'fullUpdate',
incrementalUpdate = 'incrementalUpdate',
dirtyState = 'dirtyState',
validationResult = 'validationResult',
keepAlive = 'keepAlive',
unknown = 'unknown'
}
export namespace MessageType {
/**
* Maps the given string to an literal of {@link MessageType}
* @param value The string to map.
* @returns the mapped message type literal. If the given string cannot be mapped to an
* exact type {@link MessageType.unknown} is returned.
*/
export function asMessageType(value: string): MessageType {
if (value in MessageType) {
return (MessageType as any)[value];
}
return MessageType.unknown;
}
}
/**
* Representation of an arbitrary model.
* @typeParam C Concrete type of the `content` property. Default is `unknown`.
*/
export interface Model<C = unknown> {
/** The uri of the model. */
modeluri: string;
/** The model content. */
content: C;
}
export namespace Model {
/**
* Guard function to check wether a given object is of type {@link Model}.
* @param object The object to check.
* @returns The given object as {@link Model} or `false`.
*/
export function is(object: unknown): object is Model {
return Type.AnyObject.is(object) && Type.isString(object, 'modeluri') && Type.isObject(object, 'content');
}
export function toString(model: Model): string {
return JSON.stringify(model, undefined, 2);
}
}
/**
* Type to describe a function that maps a message to a specific type.
*/
export type Mapper<M, D = unknown> = (message: M) => D;
/**
* A Mapper which directly returns the message.
*/
export const IdentityMapper: Mapper<any, any> = m => m;
/**
* Type to describe a function that maps the {@link ModelServerMessage.data} property to a specific type.
*/
export type MessageDataMapper<D = unknown> = (message: ModelServerMessage) => D;
/**
* A collection of utility functions to map the `data` property of a {@link ModelServerMessage} to a specific type.
* If the `data` object of the given message cannot be mapped to the desired type an error is thrown.
*/
export namespace MessageDataMapper {
/**
* Maps the {@link ModelServerMessage.data} property of the given message to string.
* @param message The message to map.
* @returns the `data` property as `string`.
*/
export function asString(message: ModelServerMessage): string {
return Type.asString(message.data);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a string[].
* @param message The message to map.
* @returns The `data` property as `string[]`.
* @throws {@link Error} if the 'data' property is not an array.
*/
export function asStringArray(message: ModelServerMessage): string[] {
return Type.asStringArray(message.data);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a boolean.
* @param message The message to map.
* @returns The `data` property as boolean or `false` if `data` is not of type `boolean`.
*/
export function asBoolean(message: ModelServerMessage): boolean {
return typeof message.data === 'boolean' ? message.data : false;
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a {@link Model}[].
* @param message The message to map.
* @returns The `data` property as `Model[]`.
* @throws {@link Error} if the 'data' property is not an array.
*/
export function asModelArray(message: ModelServerMessage): Model[] {
return Type.asModelArray(message.data);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a URI[].
* @param message The message to map.
* @returns The `data` property as `URI[]`.
* @throws {@link Error} if the 'data' property is not an URI array.
*/
export function asURIArray(message: ModelServerMessage): URI[] {
return Type.asURIArray(message.data);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to an {@link AnyObject}.
* @param message The message to map.
* @returns The `data` property as `AnyObject`.
* @throws {@link Error} if the 'data' property is not of type `object`.
*/
export function asObject(message: ModelServerMessage): Type.AnyObject {
return Type.asObject(message.data);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to the desired type if the data object passes the
* check with the given typeguard successfully.
* @param message The message to map.
* @param typeGuard A type guard function to check wether the data object is of the desired type.
* @typeParam T Concrete type to which the message should be mapped.
* @returns The `data` property as the desired type
* @throws {@link Error} if the check with the given typeguard fails.
*/
export function as<T>(message: ModelServerMessage, guard: Type.TypeGuard<T>): T {
return Type.asType(message.data, guard);
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a `boolean` indicating whether the message
* has the {@link MessageType.success} type.
* @param message The message to map.
* @returns `true` if the type of the message is {@link MessageType.success}, `false` otherwise.
*/
export function isSuccess(message: ModelServerMessage): boolean {
return message.type === 'success';
}
/**
* Maps the {@link ModelServerMessage.data} property of the given message to a {@link ModelUpdateResult}, indicating
* if the edit operation was successful (success: true), and if it was, how to patch the original model
* to get the updated version of the model.
*
* @param message The message to map.
* @returns a {@link ModelUpdateResult} indicating if the operation was successful, and how to patch the local
* model to get the new model if it was.
*/
export function patchModel(message: ModelServerMessage): ModelUpdateResult {
if (isSuccess(message)) {
const data = message.data as any;
const patch = data ? data.patch : undefined;
const allPatches = data ? data.allPatches : undefined;
if (patch || allPatches) {
return {
success: isSuccess(message),
patch,
patchModel: (oldModel, copy, modeluri) => {
const modelToPatch = copy ? deepClone(oldModel) : oldModel;
const patchToApply = modeluri ? getPatch(allPatches, modeluri) : Operations.isPatch(patch) ? patch : undefined;
return patchToApply
? (jsonpatch.applyPatch(modelToPatch, patchToApply).newDocument as ModelServerElement)
: modelToPatch;
},
allPatches
};
} else {
return { success: true };
}
} else {
return { success: false };
}
}
function getPatch(patches: ModelPatch[], modeluri: URI): Operation[] | undefined {
return patches.find(mp => mp.modelUri === modeluri.toString())?.patch;
}
}