Dynamic Model loading in BAP Client

Background/Usecase:

I’m creating a showcase for the public sector which features the camunda workflow engine in combination with A12. The goal is to create a Low Code Platform where you can deploy and execute custom workflows with integrated A12 forms. The workflows as well as the forms can be uploaded to the server(s) at runtime. The missing part is the connection between steps in workflows and the form to show. I can encode this information in my workflow and get all relevant information.

Problem:

The BAP loadModels saga doesn’t support dynamic data loading. As far as I understand the source of the saga the generator function try to extract the model descriptor information from the onExit and onEnter parts of the scene defined in the app model. It then uses this model descriptor as well as the instance information (which is forwarded through the activity descriptor) to load the models. This way the model information has to be hard coded in the app model and is static to the application.

In order to have no code written (and no new software version to be released) for a new or updated workflow, I need a way to dynamically load the models, e.g. give the model descriptor next to the instance id through the activity descriptor.

Is there a way to implement this at the moment?

The BAP loadModels saga does dynamic data loading for models except the application model. This has to be handled currently by the developer. However, an action ModelActions.setAppicationModel (Typedoc [1.0.1]<INTERNAL_LINK> | [2.2.0]<INTERNAL_LINK> | [3.1.0]<INTERNAL_LINK>) to set the application model exists since version 1.0.0. Hence the application model is not static, because you can change the application model with an dynamic generated one.

I hope this helps you to achieve your desired behavior.

I think we have a misunderstanding what I mean with dynamic.

The source code of the saga is like this:

if (sceneReference !== undefined) {

			// find all model descriptors from the case match,
			// we try to load all models we find as long as they are not already
			// in the model store slice
			const modelDescriptors: Model.Descriptor[] = yield select(InternalModelSelectors.modelDescriptors(sceneReference));

			const loadingConfig: ModelLoadingConfig = {
				modelDescriptors,
				activityDescriptor,
				activityId,
				dataHandlers,
				user,
				sceneReference
			};

			yield all([
				call(loadDocumentModels, loadingConfig),
				call(loadFormModels, loadingConfig),
				call(loadOverviewModels, loadingConfig)
			]);
		}
modelDescriptors(sceneReference: ApplicationModel.SceneReference) {
		return (state: object) => {

			const scene = InternalModelSelectors.sceneByReference(sceneReference)(state);

			const result: Model.Descriptor[] = [];
			if (scene.sceneChange && scene.sceneChange.onEnter) {
				result.push(...InternalModelSelectors.collectModelDescriptors(scene.sceneChange.onEnter));
			}
			if (scene.sceneChange && scene.sceneChange.onExit) {
				result.push(...InternalModelSelectors.collectModelDescriptors(scene.sceneChange.onExit));
			}
			return result;
		};
	}

The modelDescriptors are collected from the app model. Since the app model is static (compile time) and I want a dynamic (runtime) data loading I can’t use the saga because I have no way to tell the saga which models should be loaded if it’s not encoded in the app models.

The workaround suggested by @stefan-cold-haze works. I’ve wrote a custom saga that manipulates the application model in order to create a dynamic load behaviour.

Could you extend your answer to show example code? <PROJECT_NAME> might need something like that in future.

Please keep in mind, that I create a prototype and do not claim that this is a good solution. I’ve created a custom saga like this:

// ... imports
import { model } from "../../appmodel";

export const UPDATE_APP_MODEL = "UPDATE_APP_MODEL";

export interface UpdateAppModelActivityDescriptor extends Activity.Descriptor {
	formKey: string;
}

export const manipulateAppModelSaga: ApplicationSaga.Descriptor = {
	canHandle: (ad: Activity.Descriptor, action: Action<PushPayload>) => {
		return isType(action, ActivityActions.push) && UPDATE_APP_MODEL === action.payload.activity.id;
	},
	handle: (action: Action<PushPayload>) => {
		const descriptor = action.payload.activity.descriptor as UpdateAppModelActivityDescriptor;
		return manipulate(descriptor);
	}
};

function* manipulate(descriptor: UpdateAppModelActivityDescriptor): SagaIterator {
	const documentModelName = descriptor.model;
	const formKey = documentModelName + "-" + descriptor.formKey;

	const applicationModel = model;

	// TODO: the scene to be changed should be a bit more dynamic and come
	// @ts-ignore
	applicationModel.modules[0].flows[0].scenes[1].sceneChange.onEnter[0].models = [
		{modelType: "form", name: formKey},
		{modelType: "data", name: documentModelName}
		];
	yield put(ModelActions.setApplicationModel({
		model: {
			modelDescriptor: {
				modelType: "APPMODEL",
				name: "APPMODEL"
			},
			loadingState: "loaded",
			data: applicationModel
		}
	}));
}

and dispatch it in the connect function to my form component.

I were working a POC regarding delivering full stack A12 application to BAs which provide a function to support easier changing application model (require dynamic application just by start/stop the server or creating a custom function from client).

Server side implementation:

  • Make a properties configuration in your server.properties.
  • Configure to point to your application_model folder or even load it from your DB.
  • Create a REST API called /applicationModel
  • The controller/service behind request to configuration key then parse our appmodel.ts then return as JSON string.

Client implementation:

  • Within your store setup you can create new action to Fetch your application model from server by call api/applicationModel
const setupActions: Action<{}>[] = [];
setupActions.push(CustomApplicationActions.FETCH_MODEL({}));

const config = ApplicationFactories.createApplicationSetup({
		model,
		dataHandlers,
		overridePlatformSagas,
		additionalMiddlewares,
		appReducer,
		customSagas,
		setupActions,
		preComputeNewDocuments: true
	});
  • Due to the fact that our appmodel.ts need to be execute during runtime to convert back to JSON Object within a saga.
  • Create a reducer to update your store please have a look at slice models/applicationmodel which contains your application. By update this branch you will dynamically update your application model or dispatch action ModelActions.setApplicationModel

Just to make it clear. Stefan’s answer is not a workaround. It’s the proper (current) solution. The only reason that we include the app model as TS code in the sample application is to make editing easier (we have no editor). Furthermore we statically bundle it because our own server can’t store the app model.

But you are free to handle both things differently. After all, it’s only a sample application.

If you as a project have further needs, please file a ticket so that we are aware of your needs and can take them into account.

please also aware of ticket <INTERNAL_LINK>

Thanks Tuan and sorry everyone. We (or actually @martin-binary-cloud) encountered some serious problems and we need some time to come up with a solution after 2019/06.