JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: api/js/Operations.js

/* eslint-disable no-use-before-define */

define([
    'module',
    'data/js/api/OperationBuilder',
    'entity/js/api/DataModel',
    'entity/js/api/Entity',
    'entity/js/api/Property',
    'operation/js/ArrayResultOperation',
    'operation/js/ErrorOperation',
    'operation/js/FieldSelectionOperation',
    'operation/js/NonLazyOperation',
    'operation/js/OperationUtils',
    'operation/js/api/Condition',
    'operation/js/api/ExpandableReference',
    'operation/js/api/Operation',
    'operation/js/api/OperationData',
    'operation/js/api/PaginationRequest',
    'operation/js/api/Sorting',
    'operation/js/query/Query'
], function (
        module,
        OperationBuilder,
        DataModel,
        Entity,
        Property,
        ArrayResultOperation,
        ErrorOperation,
        FieldSelectionOperation,
        NonLazyOperation,
        OperationUtils,
        Condition,
        ExpandableReference,
        Operation,
        OperationData,
        PaginationRequest,
        Sorting,
        Query
    ) {

    'use strict';

    /**
     * Object representing single record.
     *
     * @typedef {Object.<String, String>} module:api/js/Operations~Record
     */

    /**
     * Operation API shorthand module.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @exports api/js/Operations
     */
    var Operations = function () {
        AbcsLib.throwStaticClassError();
    };

    Operations._LOGGER = Logger.get(module.id);
    Operations._UNSUPPORTED_INPUT = 'There is no Operation capable to handle your input parameters. Please check with the author of the given Entity, to understand what operations are available and what they support and require.';
    Operations._UNSUPPORTED_RECORD = function(missingValues) {
        return 'There is no Operation capable to handle your request. It seems to be because your record don\'t contain values (' + missingValues + ') which are required by the found operation. Please include them into your payload and try to perform the operation again.';
    };
    Operations._UNSUPPORTED_OPERATION = function(type) {
        return 'There is no Operation of type "' + type + '" . Please check with the author of the given Entity, to understand what operations are available and what they support and require.';
    };
    Operations._UNSUPPORTED_FIELD_SELECTION = function(fields) {
        var fieldIDs = fields.map(function(field) {
            return field.getId();
        });
        return 'Abcs.Operations().read(..) method had been called with an explicit field selection (' + fieldIDs + ') but underlying Operation don\'t support this feature. Result will still be filtered using your field selection but be aware that the payload itself contains full record. If you would like this to be improved, please check with the author of the underlying BOP provider and ask him for an improvement.';
    };
    Operations._NON_EXISTING_FIELD = function(fields, nonExistingFieldID) {
        var fieldIDs = fields.map(function(field) {
            if (AbcsLib.isString(field)) {
                return field;
            }
            return field.getId();
        });
        return 'Abcs.Operations().read(..) method had been called with an explicit field selection (' + fieldIDs + ') but field with ID "' + nonExistingFieldID + '" does not exist in the Entity itself. Please pass only String values corresponding to existing Properties or pass Property instances directly.';
    };

    var getOperations = function () {
        return DataModel.getInstance().operations();
    };

    /**
     * Creates an instance of 'create' operation.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @param {Object} params - Object literal with all possible parameters.
     * @param {entity/js/api/Entity} params.entity - Entity which is this operation working over
     * @param {module:api/js/Operations~Record} params.record - Data of the record that is supposed to be created.
     * @returns {operation/js/api/Operation}
     *
     * @example
     * <caption>Creates and performs operation which insert new record of Employee entity</caption>
     * var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     * var operation = Abcs.Operations().create({
     *     entity: employee,
     *     record: {
     *         firstName: 'Martin',
     *         lastName: 'Janicek',
     *         age: 28
     *     }
     * });
     *
     * operation.perform().then(function(operationResult) {
     *     if (operationResult.isSuccess()) {
     *         // Insert code you want to perform after record being created
     *     }
     * }).catch(function(operationResult) {
     *     if (operationResult.isFailure()) {
     *         // Insert code you want to perform if record creation failed
     *     }
     * });
     */
    Operations.create = function(params) {
        var mandatoryKeys = ['entity', 'record'];

        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkObjectLiteral(params, mandatoryKeys, mandatoryKeys);
        AbcsLib.checkDataType(params.entity, Entity);
        AbcsLib.checkDataType(params.record, AbcsLib.Type.OBJECT);

        var operation;
        if (getOperations().canCreate(params.entity)) {
            operation = new OperationBuilder(params.entity, Operation.Type.CREATE).get();
        }
        return checkAndUpdateOperation(operation, params.record, Operation.Type.CREATE);
    };

    /**
     * Creates an instance of 'update' operation.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @param {Object} params - Object literal with all possible parameters.
     * @param {entity/js/api/Entity} params.entity - Entity which is this operation working over
     * @param {module:api/js/Operations~Record} params.record - Data of the record that is supposed to be updated.
     * @returns {operation/js/api/Operation}
     *
     * @example
     * <caption>Creates and performs operation which updates already existing record of Employee entity</caption>
     * var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     * var operation = Abcs.Operations().update({
     *     entity: employee,
     *     record: {
     *         firstName: 'Martin',
     *         lastName: 'Janicek',
     *         age: 29
     *     }
     * });
     *
     * operation.perform().then(function(operationResult) {
     *     if (operationResult.isSuccess()) {
     *         // Insert code you want to perform after record being updated
     *     }
     * }).catch(function(operationResult) {
     *     if (operationResult.isFailure()) {
     *         // Insert code you want to perform if record update failed
     *     }
     * });
     */
    Operations.update = function(params) {
        var mandatoryKeys = ['entity', 'record'];

        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkObjectLiteral(params, mandatoryKeys, mandatoryKeys);
        AbcsLib.checkDataType(params.entity, Entity);
        AbcsLib.checkDataType(params.record, AbcsLib.Type.OBJECT);

        var entity = params.entity;
        var operation;
        if (getOperations().canUpdate(params.entity) || params.entity.hasExtEntity()) {
            operation = new OperationBuilder(entity, Operation.Type.UPDATE).get();
        }
        var record = OperationUtils.prepareDataForSave(entity, params.record);
        return checkAndUpdateOperation(operation, record, Operation.Type.UPDATE);
    };

    /**
     * Creates an instance of 'delete' operation.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @param {Object} params - Object literal with all possible parameters.
     * @param {entity/js/api/Entity} params.entity - Entity which is this operation working over
     * @param {module:api/js/Operations~Record} params.record - Data of the record that is supposed to be deleted.
     * @returns {operation/js/api/Operation}
     *
     * @example
     * <caption>Creates and performs operation which delete a single record of Employee entity</caption>
     * var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     * var operation = Abcs.Operations().delete({
     *     entity: employee,
     *     record: {
     *         id: 1
     *     }
     * });
     *
     * operation.perform().then(function(operationResult) {
     *     if (operationResult.isSuccess()) {
     *         // Insert code you want to perform after record being deleted
     *     }
     * }).catch(function(operationResult) {
     *     if (operationResult.isFailure()) {
     *         // Insert code you want to perform if record deletion failed
     *     }
     * });
     */
    Operations.delete = function(params) {
        var mandatoryKeys = ['entity', 'record'];

        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkObjectLiteral(params, mandatoryKeys, mandatoryKeys);
        AbcsLib.checkDataType(params.entity, Entity);
        AbcsLib.checkDataType(params.record, AbcsLib.Type.OBJECT);

        var operation;
        if (getOperations().canDelete(params.entity)) {
            operation = new OperationBuilder(params.entity, Operation.Type.DELETE).get();
        }
        return checkAndUpdateOperation(operation, params.record, Operation.Type.DELETE);
    };

    /**
     * Takes the given {@link operation/js/api/Operation Operation}, checks if it's properly supported and possibly transform it into ErrorOperation.
     *
     * @param {operation/js/api/Operation} operation
     * @param {Object} record
     * @param {Operation.Type} type
     * @returns {operation/js/api/Operation}
     */
    function checkAndUpdateOperation(operation, record, type) {
        var operationData = new OperationData({
            record: record
        });
        if (operation) {
            if (operation.supports(operationData)) {
                operation.setInputData(operationData);
            } else {
                var missingValues = operation.getMissingValues(record);
                operation = new ErrorOperation(Operations._UNSUPPORTED_RECORD(missingValues));
            }
        } else {
            operation = new ErrorOperation(Operations._UNSUPPORTED_OPERATION(type));
        }
        return operation;
    }

    /**
     * Reads all records matching the given restrictions.
     *
     * @AbcsAPI stable
     * @version 15.4.5
     * @param {Object} params - Object literal with all possible parameters.
     * @param {entity/js/api/Entity} params.entity - {@link entity/js/api/Entity Entity} which is created {@link operation/js/api/Operation Operation} working on
     * @param {entity/js/api/Property[]} [params.properties] - Optional sub-set of {@link entity/js/api/Property Properties} that should be returned by the {@link operation/js/api/Operation Operation}
     * @param {operation/js/api/Sorting} [params.sortBy] - {@link operation/js/api/Sorting Sorting} to get records in certain order.
     * @param {operation/js/api/ExpandableReference | operation/js/api/ExpandableReference[]} [params.expand] - Either single instance or an array of {@link operation/js/api/ExpandableReference ExpandableReference}s, one for each reference you want to expand.
     * @param {operation/js/api/Condition} [params.condition] - {@link operation/js/api/Condition Condition} to fetch only records matching the given criteria.
     * @param {operation/js/api/PaginationRequest} [params.pagination] - {@link operation/js/api/PaginationRequest PaginationRequest} to fetch only certain subset of records.
     * @param {Object} [params.customData] - Non-standard custom data which {@link operation/js/api/Operation Operation} author can use any way (s)he need.
     *                                       To learn what an {@link operation/js/api/Operation Operation} accepts as custom parameters, consult documentation for the BOP you imported into your app.
     * @returns {operation/js/api/Operation}
     *
     * @see {@link entity/js/api/Entity Entity}
     * @see {@link operation/js/api/Sorting Sorting}
     * @see {@link operation/js/api/Condition Condition}
     * @see {@link module:operation/js/api/Conditions Conditions}
     * @see {@link operation/js/api/PaginationRequest PaginationRequest}
     *
     * @example
     * <caption>
     *  Creates and performs {@link operation/js/api/Operation Operation} which reads multiple records of the Employee {@link entity/js/api/Entity entity}
     *  restricted by the given {@link operation/js/api/Condition Condition}.
     * </caption>
     *
     * require([
     *     'operation/js/api/Conditions',
     *     'operation/js/api/Operator'
     * ], function(Conditions, Operator) {
     *     var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     *     var firstName = employee.getProperty('firstName');
     *     var condition = Conditions.SIMPLE(firstName, Operator.EQUALS, 'Martin');
     *     var operation = Abcs.Operations().read({
     *         entity: employee,
     *         condition: condition
     *     });
     *
     *     operation.perform().then(function(operationResult) {
     *         if (operationResult.isSuccess()) {
     *             // Insert code you want to perform after records are fetched
     *         }
     *     }).catch(function(operationResult) {
     *         if (operationResult.isFailure()) {
     *             // Insert code you want to perform if fetching of records failed
     *         }
     *     });
     * });
     *
     * @example
     * <caption>
     *  Creates and performs {@link operation/js/api/Operation Operation} reading records of the Employee {@link entity/js/api/Entity entity} sorted by their firstname in ascending order.
     * </caption>
     *
     * require([
     *     'operation/js/api/Sorting'
     * ], function(Sorting) {
     *     var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     *     var firstName = employee.getProperty('firstName');
     *     var operation = Abcs.Operations().read({
     *         entity: employee,
     *         sortBy: Sorting.ascending(firstName)
     *     });
     *
     *     operation.perform().then(function(operationResult) {
     *         // Insert code for Success/Failure handling
     *     });
     * });
     *
     * @example
     * <caption>
     *  Fetch second page (assuming page is 15 records) of employees with firstname 'Martin'.
     * </caption>
     *
     * require([
     *     'operation/js/api/Conditions',
     *     'operation/js/api/Operator',
     *     'operation/js/api/PaginationRequest'
     * ], function(
     *         Conditions,
     *         Operator
     *         PaginationRequest
     * ) {
     *     var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     *     var firstName = employee.getProperty('firstName');
     *     var condition = Conditions.SIMPLE(firstName, Operator.EQUALS, 'Martin');
     *     var pagination = PaginationRequest.createStandard({
     *         offset: 15,
     *         pageSize: 15
     *     });
     *     var operation = Abcs.Operations().read({
     *         entity: employee,
     *         condition: condition,
     *         pagination: pagination
     *     });
     *
     *     operation.perform().then(function(operationResult) {
     *         // Insert code for Success/Failure handling
     *     });
     * });
     *
     * @example
     * <caption>
     *  Fetch all employee records and expand reference to Department so that it don't contain just ID but instead the whole department record is embedded.
     * </caption>
     *
     * require([
     *     'operation/js/api/ExpandableReference'
     * ], function(
     *         ExpandableReference
     * ) {
     *     // Creates an expandable reference for all relations between Employee and Department entities
     *     var employee = Abcs.Entities().findById('my.custom.bop.Employee');
     *     var department = Abcs.Entities().findById('my.custom.bop.Department');
     *     var deptRef = ExpandableReference.create(department);
     *
     *     var operation = Abcs.Operations().read({
     *         entity: employee,
     *         expand: deptRef
     *     });
     *
     *     operation.perform().then(function(operationResult) {
     *         // Insert code for Success/Failure handling
     *         // Result will contain main data with Employees and also each Employee will have Department record embedded
     *     });
     * });
     */
    Operations.read = function(params) {
        var mandatoryKeys = ['entity'];
        var allowedKeys = mandatoryKeys.concat([
            'sortBy',
            'expand',
            'condition',
            'pagination',
            'properties',
            'customData'
        ]);
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkObjectLiteral(params, allowedKeys, mandatoryKeys);
        AbcsLib.checkDataType(params.entity, Entity);

        var entity = params.entity;
        var fields = params.properties;
        if (fields) {
            AbcsLib.checkDataType(fields, AbcsLib.Type.ARRAY);

            // Check whether the given values are only Properties or Strings (which in fact correspond to the BOs property IDs)
            fields.forEach(function(field) {
                AbcsLib.checkDataType(field, [Property, AbcsLib.Type.STRING]);
            });

            // Transform given String values to Property instances
            fields = fields.map(function(fieldValue) {
                if (AbcsLib.isString(fieldValue)) {
                    var property = entity.getProperty(fieldValue);
                    if (property) {
                        return property;
                    } else {
                        throw new Error(Operations._NON_EXISTING_FIELD(fields, fieldValue));
                    }
                }
                return fieldValue;
            });
        }
        if (params.sortBy) {
            AbcsLib.checkDataType(params.sortBy, Sorting);
        }
        if (params.condition) {
            AbcsLib.checkDataType(params.condition, Condition);
        }
        if (params.pagination) {
            AbcsLib.checkDataType(params.pagination, PaginationRequest);
        }

        var expandedReferences = params.expand;
        if (expandedReferences) {
            AbcsLib.checkDataType(expandedReferences, [AbcsLib.Type.ARRAY, ExpandableReference]);

            // If this is single instance, turn it into an Array so that the execution will remain the same
            if (expandedReferences instanceof ExpandableReference) {
                expandedReferences = [expandedReferences];
            }

            expandedReferences.forEach(function(exandableReference) {
                AbcsLib.checkDataType(exandableReference, ExpandableReference);
            });
        }

        var queryDesc = DataModel.getInstance().queries().getQueryByAnything(entity);
        var query = new Query(queryDesc, params.condition);

        var operation;
        if (expandedReferences && expandedReferences.length > 0) {
            var builder = new OperationBuilder(entity, Operation.Type.READ_MANY);

            for (var i = 0; i < expandedReferences.length; i++) {
                builder.expandReference(expandedReferences[i]);
            }
            var compositeOperation = builder.get();
            operation = new NonLazyOperation(compositeOperation);
        } else {
            // Only one call has to be made --> No composite required
            var readMany = getOperations().canRetrieveMany(entity, query);
            if (readMany) {
                operation = new OperationBuilder(entity, Operation.Type.READ_MANY).get();
            } else {
                // If no read-many operation is available, look for read-one that supports given input
                var readOne = getOperations().canRetrieveOne(entity, query);
                if (readOne) {
                    readOne = new OperationBuilder(entity, Operation.Type.READ_ONE).get();
                    operation = new ArrayResultOperation(readOne);
                }
            }
        }

        // If field selection was requested but the operation don't support it, wrap it with automatic field selection
        // Of course, this don't save much to client, the payload is still larger due to the missing capability of the
        // back-end itself, but at least the results are consistent from the client perspective.
        if (fields && !operation.getInput().isFieldSelectionSupported()) {
            Operations._LOGGER.info(Operations._UNSUPPORTED_FIELD_SELECTION(fields));
            operation = new FieldSelectionOperation(operation, fields);
        }

        if (operation) {
            operation.setInputData(new OperationData({
                query: query,
                fields: fields,
                sorting: params.sortBy,
                customData: params.customData,
                paginationRequest: params.pagination
            }));
        } else {
            return new ErrorOperation(Operations._UNSUPPORTED_INPUT);
        }

        return operation;
    };

    return Operations;

});