DOC
APIs documented in this chapter are described in Sections:
Except for the frontend service API, described independently in the Web Client Library documentation, DOC
provides users with:
An OpenAPI documentation that describes the aforementioned services endpoints, as well as the input parameters and response format. For convenience, an OpenAPI file is provided to allow the generation of a custom client for any language supported using OpenAPI Generator. For more details, refer to the official OpenAPI and OpenAPI Generator documentations.
A JavaDoc documentation that describes the APIs of the Java classes, methods, fields, and other code elements within DOC
.
Note that, to interact with |
The Data Service provides a RESTful API to manage the data associated with each scenario. For more details, refer to Chapter Understanding the Data Service.
The API endpoints are described in the Data Service OpenAPI documentation. For convenience, the Data Service OpenAPI specification file can be converted to most languages using OpenAPI Generator.
The Data Service API can be invoked from Java through the client class DataServiceClient
interface. This interface is provided by the library com.decisionbrain.gene:data-service-client
. It must be configured by including the annotation @EnableDataServiceClient
in the configuration of your microservice. For more details, refer to the DataServiceClient JavaDoc.
To be able to use this API, import the library data-service-client
:
dependencies { implementation 'com.decisionbrain.gene:data-service-client:${versions.decisionbrain.dbgene}' }
Be sure to configure the Data Service URL and port in the application.yml
of your service. Here is an example of the configuration:
services: data: host: localhost port: 8081
To communicate with the web client, the Data Service uses the GraphQL framework.
By default, Queries (to read data) and Mutations (to alter data) are generated at application startup and defined in the application GraphQL schema.
Note that, in a development environment, you can explore exposed types, queries, and mutations using the graphiql interface. It is available from the data server URL path /graphiql, by default http://localhost:8082/graphiql. |
To have a better idea, some of the default queries that are generated for a given entity (called Entity1
, for example) are described in Section Understanding GraphQL Default Queries.
There are different ways to use GraphQL queries on scenario data. For more details, refer to Sections Understanding GraphQL Default Mutations and Understanding GraphQL Custom Queries.
Data from different scenarios of the same type can be compared. For more details, refer to Section Understanding GraphQL Scenario Comparison.
Finally, the GraphQL "introspection" feature can be enabled during development and disabled in deployments. For more details, refer to Section Understanding GraphQL Introspection.
Queries
Entity1
: returns a list of one or more instances of entity Entity1
.
getEntity1Connection
: returns a paginated, sortable and filterable list of Entity1
instances.
Example:
query { getEntity1Connection (paginationRequest: {scenarioId: "001", page: 0, size: 100, sort: []}, geneContext: {scenarioIds: ["001"]}) { totalElements totalPages content { dbGeneInternalScenarioId field1 resource { day numberOfHours dbGeneInternalId } } scenarioId } }
getEntity1Aggregation
: returns the result of one or more aggregation queries. Each numeric aggregation query can aggregate values through one or multiple nested levels.
Also, aggregation can be computed to compare values between distinct scenarios. For that, "reference" scenarios should be defined in the context. In a composite data model, the "reference" scenarios must be of the same type.
Example:
query {
getEntity1Aggregations (
aggregationRequest: [{
aggregationName: "New Series 1", aggregationKind: "count", fieldName: "numberOfHours", groupByFieldsNames: ["day", "numberOfHours"]
}],
geneContext: {
scenarioIds: ["scenario1"],
referenceScenarioIds: ["scenario2", "scenario3"]
}
) {
aggregations {
name
bucketDefinitions {
name
parents
}
buckets {
buckets
values
}
}
}
}
The result of a numeric aggregation query can be computed using one of the following operators:
COUNT
: Returns the total number of items in a collection or list. Useful for determining the size of a dataset or subset based on filters.
DISTINCT
: Returns the unique values within a field across items in the collection. This is used to remove duplicates and find the number of unique entries.
MIN
: Returns the minimum (smallest) value of a field in the collection. Typically used with numeric or date fields to identify the lowest entry.
MAX
: Returns the maximum (largest) value of a field in the collection. Useful for identifying the highest value in numeric or date fields.
AVG
: Calculates the average value of a numeric field across all items in the collection. Useful for finding the mean value, such as an average rating or price.
SUM
: Adds up the values in a numeric field for all items in the collection. Useful for totals, such as total revenue, quantity, or distance.
The result of an aggregation is represented in the form of a list of buckets, and a hierarchy definition to define how these buckets are related.
This allows us to configure Charts using series related to multiple fields (categories), using the chart configuration wizard.
![]() |
It is important to note that there is no specific GraphQL configuration, required by the application designer, to connect DOC
components to Data Service entities. When the user selects the related entity(ies)/field(s) via the web client wizards, the arguments and results of GraphQL queries are automatically connected to the web client components.
For more details, refer to Chapter Understanding the Web Client.
The Data Service exposes a single GraphQL mutation named geneTransactionalQuery
that is used for all exposed entity modifications (Create/Update/Delete) through a transactional query. This mutation handles data conflicts.
Note that all GraphQL APIs are also available through the Data Service REST API. For more details, refer to Section Understanding the Data Service API. |
The mutation geneTransactionalQuery
takes the following arguments:
dbGeneInternalScenarioId
: id of the scenario on which the operation will be performed
geneTransactionalQuery
: JSON object describing operations to perform (see below)
evaluateConflicts
: an optional boolean parameter to decide whether the Data Service should evaluate conflicts, before processing saving the changes. (default: false)
GeneTransactionalQuery
type is defined as follows:
export interface GeneTransactionalQuery { /** * List of elementary create operations */ creates?: GeneElementaryCreateQuery[]; /** * List of elementary delete operations */ deletes?: GeneElementaryDeleteQuery[]; /** * List of elementary update operations */ updates?: GeneElementaryUpdateQuery[]; }
As you can see this interface defines the changes that we want to occur on the Data Service. The types are pretty simple key/value descriptions of the entities and fields to update:
/** * Describes an elementary entity delete query. */ export interface GeneElementaryDeleteQuery { /** * The internal id of the entity to delete */ dbGeneInternalId: string; /** * JPA Entity type to delete */ entityType: string; /** * The create entities field values are a list of fieldName/fieldValue. * Field values are serialized as String so they can go through the GraphQL * layer. * * In a delete query they are only used if evaluateConflicts is set on * the query parameters, and should only contain localValue. */ fieldValues?: GeneTransactionalFieldValue[]; } /** * Describes an elementary entity create query. */ export interface GeneElementaryCreateQuery { /** * JPA Entity type to update */ entityType: string; /** * The create entities field values are a list of fieldName/fieldValue. * Field values are serialized as String so they can go through the GraphQL * layer generically. */ fieldValues: GeneTransactionalFieldValue[]; } /** * Describes an elementary entity update query, adds the id of existing entity */ export interface GeneElementaryUpdateQuery extends GeneElementaryFieldValuesQuery { /** * JPA Entity type to update */ entityType: string; /** * The create entities field values are a list of fieldName/fieldValue. * Field values are serialized as String so they can go through the GraphQL * layer generically. */ fieldValues: GeneTransactionalFieldValue[]; /** * The internal id of the entity to update */ dbGeneInternalId: string; } /** * GeneTransactionalFieldValue in transactional query represents a field with different values: * * - newValue: the value we want to set through a transactional query * - localValue: the locally known value for this field (only used to evaluate conflicts) * - remoteValue: when receiving a response from the server with a conflict, remoteValue is at this time the current value in database that is different from localValue */ export interface GeneTransactionalFieldValue { /** * Then entity fieldName */ fieldName: string; /** * The new field value. This value is set using a transactional query * of type CREATE or UPDATE, and contains the requested new value for this field. */ newValue?: string | null; /** * The current known field value if any. Use to detect conflicts while * persisting updates. * * This field is set using a transactional query of type UPDATE or DELETE. * * This local value will be compared against database value to detect conflicts. If the localValue * is different from database value a conflict will be returned containing both localValue (outdated) * and remoteValue (current database value). */ localValue?: string | null; /** * Represent the current database field value. * * This field is only set by data-service when returning a conflict, and is not used in a query. */ remoteValue?: string | null; }
The GeneElementaryUpdateQuery
interface describes the update of a single entity. The update can modify multiple fields of the same entity at the same time.
Note that the updated fields are either:
|
Here is an example for GeneElementaryUpdateQuery
:
{ entityType: 'Schedule', dbGeneInternalId: 'dsdfsdfsd-sdfsd-fsdrfergerg-juyjdcsd', fieldValues: [ { fieldName: 'name', newValue: 'Schedule1-modified' // Update the String type field }, { fieldName: 'activity', newValue: 'ddzedzesd-kpopopoopo-juyjdcsd-poi32fff' // Update the Activity type field } ] }
This mutation returns an instance of GeneTransactionalQueryResponse
having following members:
error
: (optional) if an error occurred, will contain a JSON description of a GeneTransactionalQueryError
deleted
: count of deleted entities
updated
: count of updated entities
created
: array of ids of newly created entities
conflicts
: (optional) array of edition conflicts if any, and if the query has been run using evaluateConflicts=true parameter. If this array is not empty, it means that no modification has been persisted, and the query conflicts need to be resolved.
The GeneTransactionalQueryError
class describes any error that occurred during the execution of a GeneTransactionalQuery
using following members:
sqlState
: SQLSTATE returned by the database if any
message
: Error Message
entity
: Type of entity that is thrown as exception during transaction commit
constraintName
: Name of the constraint if a constraint violation happened
sqlQuery
: SQL query that is thrown as exception if the database returned an error
As a developer, while working in the frontend Angular application you can modify data using Transactional Queries with the help of the GeneDataApiService.runTransactionalQuery(...)
and GeneDataApiService.runTransactionalQueryWithConflicts(...)
methods. Find below some examples of data modification using this API.
/** * Runs a transactional query without conflict evaluation. Without conflict evaluation means that: * Your query may overwrite data with out-of sync values. * * @param transactionalQuery gene transactional query object to execute * @param scenarioId gene scenario id of the item to modify * @param source optional component source of this query. * If the source is a GeneBaseDataWidget, then it will not reload data on the DATA_UPDATED event * corresponding to this query. * @param options the transactional query options regarding conflict detection and schema checker (default options are enabling conflict detection, and schema checker run) */ runTransactionalQuery(scenarioId: string, transactionalQuery: GeneTransactionalQuery, source: any = undefined, options: GeneTransactionalQueryOptions = DEFAULT_TRANSACTIONAL_QUERY_OPTIONS): Observable<GeneTransactionalQueryResponse> /** * Parameters object for TransactionalQuery */ export type GeneTransactionalQueryOptions = { /** * When this flag is set to true the query will first evaluate conflicts before * persisting modification, and return conflict list if any without modifying data */ evaluateConflicts: boolean; /** * Show or hide conflict editor when a conflict is detected. * If this flag is set to false, nothing will happen and a custom event handler * has to be set up to implement a custom processing of the conflict. * * If evaluateConflicts is false, this flag has no effect. */ showConflictEditor: boolean; /** * Decide the logic while saving a transactional query about the Schema Checkers * run: NO_RUN, ASYNC_RUN (default), SYNC_RUN. */ schemaCheckersRunOption: SchemaCheckersRunOption; }
Here is an example for a Plant
entity with lat
and lng
properties representing its location that we want to update:
import {GeneDataApiService} from "@gene/data"; // ... function updatePlant(dataApi: GeneDataApiService, scenarioId: string, newLatitude: string, newLongitude: string) { dataApi.runTransactionalQuery(scenarioId, { updates: [{ dbGeneInternalId: item.entity.dbGeneInternalId, entityType: 'Plant', fieldValues: [ { fieldName: 'lat', newValue: newLatitude }, { fieldName: 'lng', newValue: newLongitude } ] }] }, this).subscribe(result => {}, error => {}); } //...
Here is an example for the deletion of a GeneIssue
entity that a user clicked in the web client. This code retrieves the currently selected scenario using GeneContextService
:
import {GeneDataApiService} from "@gene/data"; /** * Deletes the issue given in parameter using currently selected scenario */ function deleteIssue(dataApi: GeneDataApiService, contextService: GeneContextService, issueToDelete : GeneEntity) { contextService.getContext() .pipe( map(c => c.scenarioIds[0]), map(scenarioId => dataApi.runTransactionalQuery(scenarioId, { creates : [], updates : [], deletes : [ { entityType : 'GeneIssue', dbGeneInternalId : issueToDelete[INTERNAL_ID_FIELD] } ] }) ) .subscribe(result => {}, error => {}); } //...
Here is an example for the creation of new entities. This method creates the Observable that will achieve that, the caller of the method has to subscribe()
to it:
import {GeneDataApiService} from "@gene/data"; function createQuery(dataApiService: GeneDataApiService, scenarioId: string, parameterName: string): Observable<GeneTransactionalQueryResponse> { const createQuery: GeneTransactionalQuery = { creates : [{ entityType : 'GeneParameter', fieldValues : [ { fieldName : 'name', newValue : parameterName }, { fieldName : 'valueType', newValue : 'n/a' }, { fieldName : 'value', newValue : 'n/a', }, { fieldName : 'explanation', newValue : 'n/a' } ] }], updates : [], deletes : [] }; return dataApiService.runTransactionalQuery(scenarioId, createQuery); } //...
In some cases, a custom function is needed to perform a specific operation/transformation on data (like manually calculating an average).
To do so DOC
offers the possibility to implement custom methods on the Data Service extension that will be automatically exposed via GraphQL, by using specific annotations on Java classes and methods. No custom mutation can be defined at the moment.
Note that, by default the web client is not allowing you to connect Data Grids to a custom query of an entity type. If you want to allow the user to configure Data Grids to use custom types, you need to activate this feature through the Application Preferences Menu
|
The first step to create a custom GraphQL query is to create a class and annotate it with @GraphQLCustomDataService
.
Every method of the bean annotated with GraphQLCustomDataSource
will correspond to a GraphQL custom query.
Any annotated method first argument should be of type GeneContext
, which contains context data such as scenarioId
. GeneContext is filled and sent automatically via the web client components, without any specific configuration.
To add extra arguments to the custom query we can use the annotation @GraphQLCustomArgument
.
For a given entity Country
defined as follows:
public class Country { public String name; public int population; }
Example 1:
If we want to return the total population of all the countries defined in the database, one would define the following bean, to perform the custom calculation and return the result:
@GraphQLCustomDataService public class CountryCustomService { private final CountryRepository countryRepository; @GraphQLCustomDataSource(graphqlName = "getCustomTotalPopulation") public Integer getTotalPopulation(GeneContext uiContext) { String scenarioId = GeneContextUtils.getScenarioId(uiContext); return countryRepository .findAllByDbGeneInternalScenarioId(scenarioId) .stream() .map(Country::getPopulation) .reduce(0, (a, b) -> a + b); } }
From this definition a new GraphQL schema query field is added where:
graphqlName
: is the name generated for the new custom query.
Using such custom definition, one can, for instance, create an interface that displays the sum of all the populations of all the countries. For more details, refer to Chapter Understanding the Web Client.
The GraphQL query that the such a widget would implement should look like the following:
query { getTotalPopulation( geneContext: {scenarioIds: ["3fc2af54-f3a9-46f3-8e1d-68fa3b6a6ef9"]} ) }
Example 2:
If we want a version of the previous method that multiplies the total population result with a given value, we would add the following method to CountryCustomService
class:
@GraphQLCustomDataSource(graphqlName = "getCustomTotalPopulationTransformed") public Integer getTotalPopulationTransformed(GeneContext uiContext, @GraphQLCustomArgument(graphQLName = "multiplyBy") Integer arg1) { return getTotalPopulation(uiContext) * arg1; }
Where:
GraphQLCustomArgument.graphQlName
is the name of the argument as it will be defined in GraphQL. Note that a different name can be used for the java method argument.
To multiply populations by 10, the syntax is as follows:
query { getCustomTotalPopulationTransformed(geneContext: {scenarioIds: ["3fc2af54-f3a9-46f3-8e1d-68fa3b6a6ef9"]}, multiplyBy: 10)}
Example 3:
Similar to previous examples, if we want to write a custom GraphQL query that returns a top 3 list of the most populated countries, we would add the following method to CountryCustomService
class:
@GraphQLCustomDataSource(graphqlName = "getTop3CountriesByPopulation") public List<Country> getTop3CountriesByPopulation(GeneContext geneContext) { String scenarioId = GeneContextUtils.getScenarioId(geneContext); return countryRepository .findAllByDbGeneInternalScenarioId(scenarioId) .stream() .sorted(Comparator.comparing(Country::getPopulation).reversed()) .limit(3) .collect(Collectors.toList()); }
To query this custom GraphQL method, we can use the following syntax:
query { getTop3CountriesByPopulation( geneContext: {scenarioIds: ["3fc2af54-f3a9-46f3-8e1d-68fa3b6a6ef9"]} ) { id, population } }
Example 4:
It is also possible to return a paginated list of countries, having the same structure returned by the default query getEntity1Connection
, as described above. To do so the following method is added to CountryCustomService
class:
@GraphQLCustomDataSource(graphqlName = "getPaginatedCountry") public Page<Country> getPaginatedCountry(GeneContext geneContext, PageRequest pageRequest) { String scenarioId = GeneContextUtils.getScenarioId(geneContext); return countryRepository.findAllByDbGeneInternalScenarioId(scenarioId, pageRequest); }
PageRequest
structure is the same as the one used for getEntity1Connection
, as described above.
The query would have the following syntax:
query {getPaginatedCountry(geneContext: {scenarioIds: ["3fc2af54-f3a9-46f3-8e1d-68fa3b6a6ef9"]}, paginationRequest: {scenarioId: "3fc2af54-f3a9-46f3-8e1d-68fa3b6a6ef9", page: 0, size: 100, sort: [{property: "id", orderBy: DESC}]}){ totalPages, content{id,population}, totalElements }}
DOC
allows querying the Data Service for scenario comparison purposes. For more details, refer to Section Understanding the Data Service Scenario Comparison.
In the following example, Activity
returns a list of one or more instances of the Activity and getActivityComparisonPage
returns a paginated, sortable and filterable list of Activity instances
query { getActivityComparisonPage ( geneContext: { scenarioIds: ["fbb146b1-3b7c-4564-b288-357fbee71d90"], referenceScenarioIds: ["01fbc776-dcc8-489d-9943-3b6785e876a2"] }, geneComparisonParameters: {includeModifiedRows: DIFF, includeAddedRows: false, includeDeletedRows: false}, dataRowsLimit: 50000, modifiedPageRequest: {page: 0, size: 50, sort: []} ){ metrics { sharedRowCount modifiedRowCount addedRowCount deletedRowCount } modified { totalPages totalElements content { id plant { country { id } plantId } durationInHours name } } } }
The API returns the following result:
{ "data": { "getActivityComparisonPage": { "metrics": { "sharedRowCount": 9000, "modifiedRowCount": 6009, "addedRowCount": 1000, "deletedRowCount": 1000 }, "modified": { "totalPages": 121, "totalElements": 6009, "content": [ { "id": ["00000001", "00000001"], "plant": { "country": { "id": ["001", "001"] }, "plantId": ["001", "001"] }, "durationInHours": [0.0, 2.0], "name": ["Activity 0", "Activity 2"] }, ... ] } } } }
The "introspection" feature allows asking a GraphQL schema for information about what queries it supports. For more details, refer to the Official GraphQL Introspection documentation.
To process scenario data, both the Scenario Service and Data Service expose GraphQL APIs. GraphQL introspection queries are disabled by default for these two services, as it is recommended for a deployed application in production. For more details, refer to this link.
However, introspection queries can be useful while the application is in development. Introspection can be enabled, preferably with a Spring development profile, in the following configuration files:
extensions/data-service-extension/src/main/resources/application.yml
extensions/scenario-service-extension/src/main/resources/application.yml
by setting the following property to true:
# Enable/Disable GraphQL introspection query (default false), should be disabled in production spring.graphql.schema.introspection.enabled: true
The Scenario Service provides a RESTful API to manage the application structure and scenario metadata. For more details, refer to Chapter Understanding the Scenario Service.
The API endpoints are described in the Scenario Service OpenAPI documentation. For convenience, the Scenario Service OpenAPI specification file can be converted to most languages using OpenAPI Generator.
The Scenario Service API can be invoked from Java through the client class ScenarioServiceClient
interface. This interface is provided by the com.decisionbrain.gene:scenario-service-client
library. It must be configured by including the annotation @EnableScenarioServiceClient
in the configuration of your microservice. For more details, refer to the ScenarioServiceClient JavaDoc.
To be able to use this API, import the library scenario-service-client
:
dependencies { implementation 'com.decisionbrain.gene:scenario-service-client:${versions.decisionbrain.dbgene}' }
Be sure to configure the Scenario Service URL and port in the application.yml
of your service. Here is an example of the configuration:
services: scenario: host: localhost port: 8081
The Execution Service manages jobs, which represent the execution of the tasks available in the application. For more details, refer to Chapter Understanding the Execution Service.
The Execution Service provides a RESTful API. For more details on the Execution Service API endpoints, refer to the Execution Service OpenAPI documentation. For convenience, the Execution Service OpenAPI specification file can be converted to most languages using OpenAPI Generator.
The Execution Service API can be invoked from Java through the client class ExecutionServiceClient
interface. For more details, refer to the ExecutionServiceClient JavaDoc. This interface is provided by the com.decisionbrain.gene:execution-service-client
library. It must be configured by including the annotation @EnableExecutionServiceClient
in the configuration of your microservice.
To be able to use this API, import the library
:execution-service-client
dependencies {
implementation 'com.decisionbrain.gene:execution-service-client
:${versions.decisionbrain.dbgene}'
}
Be sure to configure the Execution Service URL and port in the application.yml
of your service. Here is an example of the configuration:
services:
execution
:
host: localhost
port: 8081
The Web Client Library provides a set of APIs to facilitate the integration of custom widgets with the different DOC
modules and with the Web Client features. For more details, refer to Chapter Creating Custom Widgets and to the Web Client Library documentation.
Take a look at the following Angular model to get details on the different APIs.
Note that,to interact with the Data Service, the web client uses GraphQL. For more details, refer to Section Understanding the Data Service API. |
The Data API exposes a service that can be injected in any Angular component. For more details, refer to the Data API documentation.
It allows interacting with scenario data.
The Data API provides services and model classes to explore the Data Model Schema of your application, to run paginated or aggregation queries, but also to run custom queries exposed by DOC
Data Service. The Data API is used by all DOC
widgets to fetch data.
The following Sample Widget implements an example of performing a query to count current scenario issues.
/** * This is a simple example of Data API usage. It performs an aggregation Query on GeneIssue type, * to count how many issue (rows) has the selected scenario. * * @param context current context containing the scenario selection * * @return an observable of the GeneIssue count for the current selected scenario. -1 if no scenario is selected */ public countIssues(context: GeneContext): Observable<number> { // If no scenario returns -1 if (context.scenarioIds.length === 0) { return of(-1); } else { const countIssuesQuery: AggregationQuery = { aggregationName : 'count_issues', fieldName : INTERNAL_ID_FIELD, aggregationKind : 'count', groupByFieldsNames : [] }; return this.dataApi.runAggregationQuery('GeneIssue', [countIssuesQuery], context) .pipe( map(result => result.aggregations[0].buckets[0].value as number) ); } }
The Data API also allows performing filtered queries, and provide helper methods to build filters. GeneContext
objects contain definitions of your filters.
/* * Example of filtering by scenario */ const context = new GeneContextBuilder() .withScenarioId('myScenarioId') .build(); /* * Example of filter on type 'Activity' having a field 'name' and 'duration' * The following filter queries activities of the scenario * having a name containing 'delivery' and a duration greater than '5' */ getFilteredContext(metaDataService: GeneMetaDataService, context: GeneContext): Observable<GeneContext> { return metaDataService.getTypeMetaData('Activity').pipe( map(activityType => { const activitiesFilter = new GeneEntityFilterBuilder(activityType); const attributesFilter = activitiesFilter .withNewClause() .withOperator('AND'); attributesFilter.withNewAttributeFilter() .withAttribute('name') .withNewCriteria() .withOperator('CONTAINS') .withValues('delivery'); attributesFilter.withNewAttributeFilter() .withAttribute('duration') .withNewCriteria() .withOperator('GREATER_THAN') .withValues('5'); return new GeneContextBuilder() .withScenarioIds(context.scenarioIds) .withEntityFilter(activitiesFilter) .build(); } )); } /* * Example of filtering 'Activity' that are not scheduled. Supposing that we have two types, Activity and Schedule, * and that Schedule references Activity through a field 'activity'. * Note that operator INCLUDED_IN does the opposite filtering. */ getUnscheduledActivitiesContext(metaDataService: GeneMetaDataService, context: GeneContext): Observable<GeneContext> { return metaDataService.getTypeMetaData('Activity').pipe( map(activityType => { const activitiesFilter = new GeneEntityFilterBuilder(activityType); const attributesFilter = activitiesFilter .withNewClause() .withOperator('AND'); // We want all Activities not having their internal id in scheduled activities // // This filter is equivalent to SQL Query: // SELECT a.* FROM ACTIVITY a WHERE dbGeneInternalId NOT IN (SELECT s.activity FROM SCHEDULE s) attributesFilter.withNewAttributeFilter() .withAttribute('dbGeneInternalId') .withNewCriteria() .withOperator('EXCLUDED_FROM') .withValue('Schedule#activity'); return new GeneContextBuilder() .withScenarioIds(context.scenarioIds) .withEntityFilter(activitiesFilter) .build(); } )); }
The Scenario API exposes a service that can be injected in any Angular component. For more details, refer to the Scenario API documentation.
It allows interacting with scenarios.
The Sample Widget implements some examples of Scenario API usage to address basic use cases:
Fetch the list of all scenarios I have access to;
Set a custom property value on the selected scenario..
/** * This method returns an Observable of SampleWidgetModel which is built * using both Scenario API and Data API. * * @param context */ public loadWidgetModel(context: GeneContext): Observable<SampleWidgetModel> { return forkJoin([ this.scenarioApi.getAllPathNodes(['scenario']), this.countIssues(context) ] ).pipe( map(([scenarios, issuesCount]) => this.buildWidgetModel(context, scenarios, issuesCount)) ); } /** * Update the Scenario object having the given scenarioUuid by setting its property SCENARIO_STATUS to the * given value. * * @param scenarioUuid * @param status */ public updateScenarioStatus(context: GeneContext, status: SampleScenarioStatus): Observable<VoidResult> { if (context.scenarioIds.length > 0) { const scenarioUuid = context.scenarioIds[0]; return this.scenarioApi.savePathProperties(scenarioUuid, {[SampleWidgetService.SCENARIO_STATUS] : status}); } else { throw Error('No scenario Selected'); } }
The Scenario API provides services and model classes to interact with the Scenario Service. Note that every exposed method is subject to access right checks. For example, you can list scenarios for which you have at least "READ" permission, but you won't be able to modify them if you do not have "WRITE" permission.
The Scenario API manipulates "nodes" that can represent different types of elements, such as scenarios, folders, workspaces, views and dashboards.
This allows to:
List all nodes of type (e.g. list all nodes of type "SCENARIOS").
Retrieve properties of a give node.
Save properties for a given node.
Change node permissions.
Create nodes
Retrieve events attached to a node
DOC
allows the applications to store "events" attached to Nodes (Scenario or any other Node objects). Events have a message, an author and a timestamp. For example, each time a Scenario is locked by the system, an event is automatically created. When the user add comments through the Scenario Timeline, an event is also attached to the Scenario.
The Scenario API provides methods to:
Retrieve the Events associated to a Node
Create Comment Events on a Node.
The Task API exposes a service that can be injected in any Angular component. For more details, refer to Task API documentation.
It allows interacting with tasks and jobs. A job is a single task execution, and a task can have multiple jobs. For more details, refer to Task Events API.
The API can be used to:
Retrieve the available Tasks (Optimization Server Tasks or Backend Tasks)
Retrieve a Task Definition (Description, version, input and output parameters, etc.)
Retrieve running or completed Jobs
Launch a new Job
Task Events are dispatched by the Framework when a Job status, progress or result changes. The application can also fire a REQUEST_RUN_NEW_JOB to request the Framework the Job Creation Wizard.
See Task Events API for more details.
The Event API exposes a service that can be injected in any Angular component. For more details, refer to the Event API documentation.
It allows listening to the different events triggered by DOC
services or user interactions. The exposed Event Service is an Event bus where any event is dispatched.
DOC
provides different types of events, which are extensible by extended base class GeneEvent
The following Sample Widget implements an example of Scenario Event handler that shows notifications each time a Scenario Event is received, for example, when the user selects a Scenario.
export class SampleWidgetComponent extends GeneBaseDataWidgetComponent<SampleWidgetConfiguration, SampleWidgetModel> { // ... /** * Overrides base method to handle Scenario Events */ protected onGeneScenarioEvent(event: GeneScenarioEvent) { this.toastrService.info('Received Scenario Event !' + event.type); } // ... }
The following types of event are available:
Application events are events that are triggered when the application state has changed, for example, when application settings have changed. For more details, refer to Application Events API documentation.
Web client events are triggered upon some of user interactions, example: Sidebar shown/hidden, entering or exiting Fullscreen, etc. For more details, refer to Web Client Events API documentation.
Data events are dispatched by the Framework when data are being modified, imported or exported. Application can also trigger REQUEST_EXCEL_IMPORT event to request the provided Excel export mechanism. For more details, refer to Data Events API documentation.
Scenario events are dispatched by the Framework when scenario or Node objects are being modified (Creation, Update, Duplicate, Delete, Lock/Unlock, Permissions Change). For more details, refer to Scenario Events API documentation.
The Context API exposes a service that can be injected in any Angular component. For more details, refer to Context API documentation.
It allows storing the application state in a serializable object. It contains:
A list of Selected Scenarios
A list of Selected entities by type (optional)
A list of Filters (optional)
This context is sent as a parameter of the Data Service requests when you are opening a View or a Dashboard. Each time the user modifies the context by modifying Scenario/Entities selection, or changing filters, the context is updated, saved through the Scenario Service, and all widgets are notified about the Context update.
As stated above DOC
provides a notion of selected entities that are propagated to the web client elements that support this feature. There are currently three modes supported in DOC
:
selection: a widget supporting this mode will be able to be the source of selection, driving the selection for the web client.
filter: in this mode a widget is supposed to only take into account entities provided in context selection.
highlight: in this mode the widget can highlight the selected entities, for instance, with another color.
A widget can take part in this behavior by implementing GeneWidgetSelectionAware
interface in a way similar to this for instance
export class SampleWidgetComponent implements GeneWidgetSelectionAware { // ... /** * Return among the context selections, the supported selection(s). * Returns an empty array if current selection is not supported (has no impact on data) on * the current widget state. * * @param context current context containing a selection */ getSupportedSelection(context: GeneContext): GeneContextSelection { // simple utility method filtering entities by the given type return GeneSelectionUtils.getSubSelection(context.selection, this._widgetConfiguration.dataType); } /** * declare which selection mode this widget supports */ getSupportedSelectionModes(): SelectionMode[] { return ['select', 'filter', 'highlight']; } /** * Get the current selection mode of the widget */ getSelectionMode(): SelectionMode { return this.selectionMode; } /** * Set the current selection mode of the widget * @param mode */ setSelectionMode(mode: SelectionMode) { // store the selection mode and take it in account when loading data this.selectionMode = mode; // request reload of widget data this.requestLoadData(); } // ... }
It is important to understand that the selected entities provided by the context API are based on what we call Business Keys, it is a combination of all the properties that allow to describe a Business Entity instance in a unique way. This is implementation is chosen among other reasons, to allow to compare same entities across different scenarios, without relying on unique generated identifiers.
When you manipulate entities obtained by the Data API, those objects do not contain business key metadata by default.
For this reason if you want to compare data in a custom component against entities provided by Context API you have to enhance them using the MetaData API, see this example of GeneBaseDataWidget.loadData()
implementation which takes into account the current selectionMode and populates all the entities with relevant business key information to be used for Context Selection support.
import { INTERNAL_BK_TOKEN_FIELD } from '@gene/data'; loadData(context: GeneContext): Observable<MyCustomType[]> { const metaModel$ = this.metaDataService.getMetaModel(); const dataObjects$ = this.dataApiService.runQuery<T[]>(context, this._widgetConfiguration.entityTypeName); return forkJoin([dataOjects$, metaModel$]).pipe( flatMap(([dataOjects, metaModel]) => { const typeMetaData = metaModel.getTypeMetaData(this._widgetConfiguration.entityTypeName); .pipe( // Important! we need to "enrich" other events with business key information in order to support filtering and highlighting tap((dataOjects: T[]) => dataOjects.forEach(e => metaModel.enrichEntityWithBusinessKeyInformations(e, type, (o) => this.colorService.getHexColorForObject(o)))), ); }) ); } // ... refreshSelection() { if (this.selectionMode === 'highlight' || this.selectionMode === 'select') { this.contextService.context.pipe(first()).subscribe(c => { const currentType = this._widgetConfiguration.dataType; const selection = GeneSelectionUtils.getSubSelection(c.selection, currentType); // grid options.api is the api to interact with ag-grid tables this.gridOptions.api.forEachNode(node => { // here is the important part: retrieve the business key token from the entities displayed in the widget const nodeBKinfo = node.data[INTERNAL_BK_TOKEN_FIELD]; // compare the business key to all the entities in current context selection const selected = selection.selectedEntities[currentType].indexOf(nodeBKinfo) >= 0; // select the node accordingly node.setSelected(selected); }); } ); } } setSelectionMode(mode: SelectionMode) { // store the selection mode and take it in account when loading data this.selectionMode = mode; // request reload of widget data this.refreshSelection(); } // ...
The Meta Data API exposes a service that can be injected in any Angular component. For more details, refer to Meta Data API documentation.
It allows getting metamodel information of your data-model types, as defined through the JDL file.
constructor(protected metaDataApi: GeneMetaDataService) { // Example of fetching information for type 'Activity' metaDataApi.getTypeMetaData('Activity').subscribe(entityType => { console.log('business key fields', entityType.getBusinessKeyFields()) ; console.log('Available queries', entityType.getQueries()); console.log('Relation fields', entityType.getRelations()); console.log('All fields', entityType.getFields()); }); }
The Action API exposes a service that can be injected in any Angular component. For more details, refer to Action API documentation.
It allows configuring and performing UI-related actions from the web client. These actions can be associated with:
Buttons in the toolbar of custom views and dashboards, using the option Configure Toolbar. For more details, refer to Section Understanding the Custom Views and Dashboards.
The Button widget. For more details, refer to Section Using the Button Widget.
Custom entries in the Actions menu available for each scenario displayed in the Scenario List widget. For more details, refer to Section Using the Scenario List Widget.
When creating a new project, several action samples are provided in web/src/app/modules/sample-actions
:
web/src/app/modules/sample-actions/actions/sample-action-copy-url.ts
allows to perform a copy to clipboard of the current application URL.
web/src/app/modules/sample-actions/actions/sample-action-hello-world-with-data.ts
allows to use parameters and dynamic data.
web/src/app/modules/sample-actions/actions/sample-action-open-webpage.ts
allows to open a URL through a new web browser page, with preconditions.
web/src/app/modules/sample-actions/actions/sample-action-scenario-info.ts
allows to display information on a scenario and, through dynamic data and parameter resolution, add it to the Actions menu in the Scenario List widget.
Actions have an ID, a name and a description. The name and the description can be translated through the i18nName
and i18nDescription
keys, respectively.
An action is defined by several parts:
A GeneActionExecutor
, which is a function that will be called to execute the action. This method will return an observable of GeneActionResult<R>
where R
is a type of result.
An optional execution parameter, which will be a value of a given type passed as a parameter of the action (if applicable).
An optional parametersConfigurator,
which, when provided, is a TypeScript class of the widget to use to configure the action parameter. The expected widget type must implement the GeneActionParametersAware
interface.
An optional parametersResolver
function, which, when provided, will resolve the parameter to use before the action execution. This is useful when an action parameter requires contextual runtime parameters that can not be configured beforehand.
An optional list of GenePrecondition
, which, when provided, are evaluated and must all return {canExecute:true}
to allow the user to execute the associated action. If one of the provided preconditions evaluates to {canExecute:false}
, the web client will not let the user execute the action.
The following sample executes an action that copies the URL of the current page.
import { GeneAction, GeneActionExecutor, GeneActionManifest } from "@gene/widget-core"; import { GeneClipboardUtils } from "@gene/widget-data"; import { of } from "rxjs"; /** * A sample action that copies the current URL to the clipboard */ export const SAMPLE_ACTION_COPY_URL: GeneActionManifest = { id: "sample-copy-url", name: "Sample Action — copy current URL", icon: 'copy' }; /** * Executor function for SAMPLE_ACTION_COPY_URL action. Copy the current URL to the clipboard. */ export const copyUrlExecutor = ((_: GeneAction<string>) => { const success = GeneClipboardUtils.copyUrlToClipboard(); return of( { success }); }) as GeneActionExecutor;
An action requires to be registered by code in order to be available in the web client at runtime. To do so, the GeneActionService#registerAction(...)
must be used.
Here are some examples of action registrations that are available in the code samples of the web-frontend
module.
Registering a simple action with no precondition: When executed, this action copies the current URL to the clipboard. It requires no preconditions and takes no parameters.
/** * A sample action that copies the current URL to the clipboard */ const SAMPLE_ACTION_COPY_URL: GeneActionManifest = { id: "sample-copy-url", name: "Sample Action — copy current URL", icon: 'copy' }; /** * Executor function for SAMPLE_ACTION_COPY_URL action. Copy the current URL to the clipboard. */ const copyUrlExecutor = ((_: GeneAction<string>) => { const success = GeneClipboardUtils.copyUrlToClipboard(); return of( { success }); }) as GeneActionExecutor; // Register the action actionService.registerAction({ manifest: SAMPLE_ACTION_COPY_URL, executor: copyUrlExecutor } as GeneActionRegistrationDescriptor<string>);
Registering an action with preconditions: This action opens a new web browser page on a parameter URL. It has the precondition of having a parameter URL set and uses the ActionTextInputConfiguratorComponent
component to configure the parameter URL.
/** * A sample action that opens a custom web page which URL is passed as parameter */ const SAMPLE_ACTION_OPEN_WEB_PAGE: GeneActionManifest = { id: "sample-open-web-page", name: "Sample Action — open a custom web page", icon: 'link', hasParameters: true }; /** * Executor function for SAMPLE_ACTION_OPEN_WEB_PAGE action. Open a new tab with the URL passed as parameter of the action. */ const openCustomWebPageExecutor = ((action: GeneAction<string>) => { const w = window.open(action.parameters, '_blank'); const success = w!=null; const result = success ? action.parameters : null; return of( { success, result }); }) as GeneActionExecutor; /** * A precondition that checks that the URL parameter is not null */ const notNullURLPrecondition : GenePrecondition<string> = { evaluate: (context: GeneContext, url?: string) => { const canExecute = url?.length > 0; const message = canExecute ? '' : 'The URL parameter must be provided.'; return of({ canExecute, message }) } }; // Register custom actions samples actionService.registerAction({ manifest: SAMPLE_ACTION_OPEN_WEB_PAGE, executor: openCustomWebPageExecutor, preconditions: [notNullURLPrecondition], parametersConfigurator: ActionTextInputConfiguratorComponent, } as GeneActionRegistrationDescriptor<string>);
Finally, actions can also be executed programmatically using GeneActionService
to provide additional data to an action, which can be combined with a parameter resolver function, as in sample-action-hello-world-with-data.ts
.
// Executing the action with runtime data to be used by the parameter resolver actionService.executeAction({manifest:SAMPLE_ACTION_HELLO_WORLD}, `Jane Doe, born on ${new Date().toLocaleDateString()}`);
If need be, the Action API can display a configurator via the method getActionConfigurator
.