@eclipse-emfcloud/model-service
Version:
Model service framework
293 lines • 13.4 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2023-2024 STMicroelectronics.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: MIT License which is
// available at https://opensource.org/licenses/MIT.
//
// SPDX-License-Identifier: EPL-2.0 OR MIT
// *****************************************************************************
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const chai_1 = __importStar(require("chai"));
const chai_like_1 = __importDefault(require("chai-like"));
const chai_things_1 = __importDefault(require("chai-things"));
const sinon_1 = __importDefault(require("sinon"));
const sinon_chai_1 = __importDefault(require("sinon-chai"));
const model_manager_1 = require("@eclipse-emfcloud/model-manager");
const trigger_engine_1 = require("@eclipse-emfcloud/trigger-engine");
const fast_json_patch_1 = require("fast-json-patch");
const lodash_1 = require("lodash");
const model_service_model_manager_1 = require("../model-service-model-manager");
chai_1.default.use(chai_like_1.default);
chai_1.default.use(chai_things_1.default);
chai_1.default.use(sinon_chai_1.default);
const alice = {
name: 'Alice',
id: '88BB5693-D51C-4BAE-9CC0-197E51304390',
registeredOn: '19-07-2023',
};
const bob = { name: 'Bob', id: 'A713B4E9-E5D5-4F23-8FF2-FD39800A08D5' };
const modelAImage = {
name: 'Customer Data',
lastRegistration: alice.registeredOn,
customers: [alice],
};
const keyboard = { sku: '11342720', name: 'keyboard' };
const mouse = { sku: '11855065', name: 'mouse' };
const aliceKeyboard = {
customer: alice.id,
sku: keyboard.sku,
date: '24-07-2023',
};
const bobMouse = { customer: bob.id, sku: mouse.sku };
const modelBImage = {
name: 'Business Data',
lastOrder: aliceKeyboard.date,
products: [keyboard, mouse],
orders: [aliceKeyboard],
};
const MODEL_A_ID = 'test://38BCC304-652F-4C27-A225-81BF6C8EDCA8/model';
const MODEL_B_ID = 'test://4CE1EB0C-99AA-4ABE-B531-7618ED1174DC/model';
const MODEL_A2_ID = 'test://7D0A76E0-BB74-4F99-9FEF-D2A66874D785/model';
describe('ModelServiceModelManager', () => {
let modelManager;
let commandStack;
const getModelA = () => modelManager.getModel(MODEL_A_ID);
const getModelB = () => modelManager.getModel(MODEL_B_ID);
beforeEach(() => {
const modelA = (0, lodash_1.cloneDeep)(modelAImage);
const modelB = (0, lodash_1.cloneDeep)(modelBImage);
modelManager = (0, model_service_model_manager_1.createModelServiceModelManager)();
modelManager.setModel(MODEL_A_ID, modelA);
modelManager.setModel(MODEL_B_ID, modelB);
modelManager.triggerEngine.addModelTrigger(customerRegistration);
modelManager.triggerEngine.addModelTrigger(orderDate);
commandStack = modelManager.getCommandStack('test');
});
describe('multiple models', () => {
it('multiple models triggered', async () => {
const addBob = (0, model_manager_1.createModelUpdaterCommand)('Add Bob', MODEL_A_ID, (model) => {
model.customers.push((0, lodash_1.cloneDeep)(bob));
});
const orderMouse = (0, model_manager_1.createModelUpdaterCommand)('Order Mouse', MODEL_B_ID, (model) => {
model.orders.push((0, lodash_1.cloneDeep)(bobMouse));
});
const commandResult = await commandStack.execute(new model_manager_1.CompoundCommandImpl('Add Bob and Order Mouse', addBob, orderMouse));
// Assertions on models
(0, chai_1.expect)(getModelA().customers).to.contain.an.item.like({
...bob,
registeredOn: '24-07-2023',
});
(0, chai_1.expect)(getModelB().orders).to.contain.an.item.like({
...bobMouse,
date: '24-07-2023',
});
// Assertions on command result
(0, chai_1.expect)(commandResult).to.exist;
const triggerCommands = Array.from(commandResult.entries()).filter(([key, _]) => key.label === 'Apply triggers');
// One per model
(0, chai_1.expect)(triggerCommands).to.have.length.at.least(2);
(0, chai_1.expect)(triggerCommands).to.contain.an.item.like([
{ modelId: MODEL_A_ID },
[
{ op: 'add', path: '/customers/1/registeredOn' },
{ op: 'test' },
{ op: 'replace', path: '/lastRegistration' },
],
]);
// The ModelB trigger didn't have to update /lastOrder
(0, chai_1.expect)(triggerCommands).to.contain.an.item.like([
{ modelId: MODEL_B_ID },
[{ op: 'add', path: '/orders/1/date' }],
]);
});
});
describe('change notifications', () => {
it('notification includes trigger changes', async () => {
const sub = modelManager.subscribe(MODEL_A_ID);
const modelChanged = sinon_1.default.stub();
sub.onModelChanged = modelChanged;
const addBob = (0, model_manager_1.createModelUpdaterCommand)('Add Bob', MODEL_A_ID, (model) => {
model.customers.push((0, lodash_1.cloneDeep)(bob));
});
await commandStack.execute(addBob);
sinon_1.default.assert.calledOnceWithMatch(modelChanged, MODEL_A_ID, getModelA(), [
// The initial addition of the object
{
op: 'add',
path: '/customers/1',
value: { name: 'Bob', id: 'A713B4E9-E5D5-4F23-8FF2-FD39800A08D5' },
},
// The trigger setting the registeredOn property of the new object
{ op: 'add', path: '/customers/1/registeredOn', value: '24-07-2023' },
sinon_1.default.match({ op: 'test' }),
{
op: 'replace',
path: '/lastRegistration',
value: '24-07-2023',
},
]);
});
});
describe('load notifications', () => {
let modelA2;
beforeEach(() => {
modelA2 = (0, lodash_1.cloneDeep)(modelAImage);
});
it('notifies on model added', () => {
const sub = modelManager.subscribe();
const modelLoaded = sinon_1.default.stub();
sub.onModelLoaded = modelLoaded;
modelManager.setModel(MODEL_A2_ID, modelA2);
sinon_1.default.assert.calledOnceWithExactly(modelLoaded, MODEL_A2_ID);
});
});
describe('unload notifications', () => {
let modelA2;
beforeEach(() => {
modelA2 = (0, lodash_1.cloneDeep)(modelAImage);
modelManager.setModel(MODEL_A2_ID, modelA2);
});
it('notifies on model removed', () => {
const sub = modelManager.subscribe();
const modelUnloaded = sinon_1.default.stub();
sub.onModelUnloaded = modelUnloaded;
const removed = modelManager.removeModel(MODEL_A2_ID);
sinon_1.default.assert.calledOnceWithExactly(modelUnloaded, MODEL_A2_ID, removed);
});
it("doesn't notify on absent model", () => {
modelManager.removeModel(MODEL_A2_ID);
const sub = modelManager.subscribe();
const modelUnloaded = sinon_1.default.stub();
sub.onModelUnloaded = modelUnloaded;
modelManager.removeModel(MODEL_A2_ID);
sinon_1.default.assert.notCalled(modelUnloaded);
});
});
describe('Corner Cases', () => {
let applyTriggers;
let exposedCoreModelManager;
let exposedModelManager;
beforeEach(() => {
exposedModelManager =
modelManager;
exposedCoreModelManager = exposedModelManager.delegate;
const workingCopyManager = exposedCoreModelManager._modelStore;
workingCopyManager.open([MODEL_A_ID, MODEL_A2_ID, MODEL_B_ID]); // Need it open to test triggers with it
applyTriggers = (commandResult) => exposedCoreModelManager.applyTriggers(workingCopyManager, commandResult);
});
it('correctly applies triggers to undefined command results', async () => {
const result = applyTriggers(undefined);
return (0, chai_1.expect)(result).eventually.not.to.exist;
});
it('correctly processes triggers for result of command that has no model', async () => {
const delta1 = [
{ op: 'add', path: '/orders/0', value: aliceKeyboard },
];
const badCommand = (0, model_manager_1.createModelPatchCommand)('test', MODEL_B_ID, () => delta1);
sinon_1.default.stub(badCommand, 'modelId').value(undefined);
getModelA().customers.push((0, lodash_1.cloneDeep)(bob));
const delta2 = [
{ op: 'add', path: '/customers/1', value: bob },
];
const goodCommand = (0, model_manager_1.createModelPatchCommand)('test', MODEL_A_ID, () => delta2);
const triggeringResults = new Map([
[badCommand, delta1],
[goodCommand, delta2],
]);
const result = await applyTriggers(triggeringResults);
(0, chai_1.expect)(result).to.exist;
(0, chai_1.expect)(result).to.be.like({
label: 'Apply triggers',
modelId: MODEL_A_ID,
state: 'ready',
});
});
it('tolerates CompoundCommands in the triggering command-result map', async () => {
const delta1 = [
{ op: 'add', path: '/orders/0', value: aliceKeyboard },
];
const badCommand = new model_manager_1.CompoundCommandImpl('test');
getModelA().customers.push((0, lodash_1.cloneDeep)(bob));
const delta2 = [
{ op: 'add', path: '/customers/1', value: bob },
];
const goodCommand = (0, model_manager_1.createModelPatchCommand)('test', MODEL_A_ID, () => delta2);
const triggeringResults = new Map([
[badCommand, delta1],
[goodCommand, delta2],
]);
const result = await applyTriggers(triggeringResults);
(0, chai_1.expect)(result).to.exist;
(0, chai_1.expect)(result).to.be.like({
label: 'Apply triggers',
modelId: MODEL_A_ID,
state: 'ready',
});
});
});
});
function set(model, path, value) {
const op = (0, fast_json_patch_1.getValueByPointer)(model, path) === undefined ? 'add' : 'replace';
return { op, path, value };
}
/** A trigger that initializes the registration date of a newly added customer. */
const customerRegistration = {
getPatch: async (modelId, model, delta) => {
const op = (0, trigger_engine_1.addOperations)(delta)[0];
if (modelId == MODEL_A_ID && op && /^\/customers\/\d+$/.test(op.path)) {
return [
set(model, `${op.path}/registeredOn`, '24-07-2023'),
set(model, '/lastRegistration', '24-07-2023'),
];
}
return undefined;
},
};
/** A trigger that initializes the date of a newly added order. */
const orderDate = {
getPatch: (modelId, model, delta) => {
const op = (0, trigger_engine_1.addOrReplaceOperations)(delta)[0];
if (op && modelId == MODEL_B_ID && /^\/orders\/\d+$/.test(op.path)) {
return [
set(model, `${op.path}/date`, '24-07-2023'),
set(model, '/lastOrder', '24-07-2023'),
];
}
return undefined;
},
};
//# sourceMappingURL=model-service-model-manager.spec.js.map