cf-node-client
momo auto_awesome BUY CLAUDE KIT WITH 20% OFF coffee BUY ME COFFEE

Source: model/cloudcontroller/ServiceInstances.js

"use strict";

const CloudControllerBase = require("./CloudControllerBase");

/**
 * Manage Service Instances on Cloud Foundry
 * Supports both Cloud Foundry API v2 and v3
 */
class ServiceInstances extends CloudControllerBase {

    /**
     * @param {String} endPoint [CC endpoint]
     * @param {Object} options [Configuration options]
     * @constructor
     * @returns {void}
     */
    constructor(endPoint, options) {
        super(endPoint, options);
    }

    /**
     * Get Service Instances (supports both v2 and v3)
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/list_all_service_instances.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#list-service-instances}
     *
     * @param {Object} filter [Filter options]
     * @return {Promise} [Promise resolving to service instances list]
     */
    getInstances(filter) {
        if (this.isUsingV3()) {
            return this._getInstancesV3(filter);
        } else {
            return this._getInstancesV2(filter);
        }
    }

    _getInstancesV2(filter) {
        const url = `${this.API_URL}/v2/service_instances`;
        let qs = filter || {};
        
        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            },
            qs: qs
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    _getInstancesV3(filter) {
        const url = `${this.API_URL}/v3/service_instances`;
        let qs = filter || {};

        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            qs: qs,
            json: true
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get a Service Instance by GUID
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/retrieve_a_particular_service_instance.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#get-a-service-instance}
     *
     * @param  {String} guid [Service Instance GUID]
     * @return {Promise} [Promise resolving to service instance]
     */
    getInstance(guid) {
        if (this.isUsingV3()) {
            return this._getInstanceV3(guid);
        } else {
            return this._getInstanceV2(guid);
        }
    }

    /**
     * Find a Service Instance by name using server-side filtering.
     * Returns the first matching resource or null if not found.
     * Optionally filter by space GUID.
     *
     * v2: Uses q=name:{name} filter (and q=space_guid:{spaceGuid} if provided)
     * v3: Uses names={name} filter (and space_guids={spaceGuid} if provided)
     *
     * @param  {String} name [Service Instance name]
     * @param  {String} [spaceGuid] [Optional space GUID to narrow search]
     * @return {Promise} [Promise resolving to service instance resource or null]
     */
    getInstanceByName(name, spaceGuid) {
        if (!name || typeof name !== "string") {
            return Promise.reject(new Error("Service instance name must be a non-empty string."));
        }
        let filter;
        if (this.isUsingV3()) {
            filter = { names: name };
            if (spaceGuid) { filter.space_guids = spaceGuid; }
        } else {
            filter = { q: [`name:${name}`] };
            if (spaceGuid) { filter.q.push(`space_guid:${spaceGuid}`); }
        }
        return this.getInstances(filter).then(result => {
            const resources = result.resources || [];
            return resources.length > 0 ? resources[0] : null;
        });
    }

    _getInstanceV2(guid) {
        const url = `${this.API_URL}/v2/service_instances/${guid}`;
        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            }
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    _getInstanceV3(guid) {
        const url = `${this.API_URL}/v3/service_instances/${guid}`;
        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            json: true
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get Service Instance Permissions (v2 only)
     * {@link http://apidocs.cloudfoundry.org/226/service_instances/retrieving_permissions_on_a_service_instance.html}
     *
     * @param  {String} guid [Service Instance GUID]
     * @return {Promise} [Promise resolving to permissions]
     */
    getInstancePermissions(guid) {
        if (this.isUsingV3()) {
            throw new Error("getInstancePermissions is not available in Cloud Foundry API v3. Use v2 API for this operation.");
        }
        return this._getInstancePermissionsV2(guid);
    }

    _getInstancePermissionsV2(guid) {
        const url = `${this.API_URL}/v2/service_instances/${guid}/permissions`;
        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            }
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Create a Service Instance
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/create_a_service_instance.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#create-a-service-instance}
     *
     * @param {Object} instanceOptions [Service Instance options]
     * @param {Boolean} [acceptsIncomplete=false] [Allow async provisioning]
     * @return {Promise} [Promise resolving to created service instance]
     */
    add(instanceOptions, acceptsIncomplete = false) {
        if (this.isUsingV3()) {
            return this._addV3(instanceOptions, acceptsIncomplete);
        } else {
            return this._addV2(instanceOptions, acceptsIncomplete);
        }
    }

    _addV2(instanceOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v2/service_instances`;
        const qs = {};
        if (acceptsIncomplete) qs.accepts_incomplete = true;

        const options = {
            method: "POST",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            },
            form: JSON.stringify(instanceOptions),
            qs
        };

        // Async provisioning returns 202, sync returns 201
        const expectedStatus = acceptsIncomplete ? this.HttpStatus.ACCEPTED : this.HttpStatus.CREATED;
        return this.REST.request(options, expectedStatus, true);
    }

    _addV3(instanceOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v3/service_instances`;
        const options = {
            method: "POST",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            json: instanceOptions
        };

        // v3 async operations return a job location header
        const expectedStatus = acceptsIncomplete ? this.HttpStatus.ACCEPTED : this.HttpStatus.CREATED;
        return this.REST.request(options, expectedStatus, true);
    }

    /**
     * Update a Service Instance
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/updating_a_service_instance.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#update-a-service-instance}
     *
     * @param  {String} guid [Service Instance GUID]
     * @param  {Object} instanceOptions [Service Instance options]
     * @param  {Boolean} [acceptsIncomplete=false] [Allow async update]
     * @return {Promise} [Promise resolving to updated service instance]
     */
    update(guid, instanceOptions, acceptsIncomplete = false) {
        if (this.isUsingV3()) {
            return this._updateV3(guid, instanceOptions, acceptsIncomplete);
        } else {
            return this._updateV2(guid, instanceOptions, acceptsIncomplete);
        }
    }

    _updateV2(guid, instanceOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v2/service_instances/${guid}`;
        const qs = {};
        if (acceptsIncomplete) qs.accepts_incomplete = true;

        const options = {
            method: "PUT",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            },
            form: JSON.stringify(instanceOptions),
            qs
        };

        const expectedStatus = acceptsIncomplete ? this.HttpStatus.ACCEPTED : this.HttpStatus.OK;
        return this.REST.request(options, expectedStatus, true);
    }

    _updateV3(guid, instanceOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v3/service_instances/${guid}`;
        const options = {
            method: "PATCH",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            json: instanceOptions
        };

        const expectedStatus = acceptsIncomplete ? this.HttpStatus.ACCEPTED : this.HttpStatus.OK;
        return this.REST.request(options, expectedStatus, true);
    }

    /**
     * Delete a Service Instance
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/delete_a_service_instance.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#delete-a-service-instance}
     *
     * @param  {String} guid [Service Instance GUID]
     * @param  {Object} [deleteOptions] [Delete options]
     * @param  {Boolean} [acceptsIncomplete=false] [Allow async deletion]
     * @return {Promise} [Promise resolving to delete result]
     */
    remove(guid, deleteOptions, acceptsIncomplete = false) {
        if (this.isUsingV3()) {
            return this._removeV3(guid, deleteOptions, acceptsIncomplete);
        } else {
            return this._removeV2(guid, deleteOptions, acceptsIncomplete);
        }
    }

    _removeV2(guid, deleteOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v2/service_instances/${guid}`;
        let qs = deleteOptions || {};
        if (acceptsIncomplete) qs.accepts_incomplete = true;

        const options = {
            method: "DELETE",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            },
            qs: qs
        };

        const expectedStatus = acceptsIncomplete ? this.HttpStatus.ACCEPTED : this.HttpStatus.NO_CONTENT;
        const jsonOutput = acceptsIncomplete; // async returns JSON job info
        return this.REST.request(options, expectedStatus, jsonOutput);
    }

    /**
     * Remove a service instance (v3 path).
     * Managed service instance deletion returns 202 (async job).
     * User-provided service instance deletion returns 204 (sync, no body).
     * The acceptsIncomplete parameter is accepted for API compatibility with the v2
     * code path but has no effect — v3 always behaves as if acceptsIncomplete=true.
     * @private
     */
    _removeV3(guid, deleteOptions, acceptsIncomplete) {
        const url = `${this.API_URL}/v3/service_instances/${guid}`;
        let qs = deleteOptions || {};

        const options = {
            method: "DELETE",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            qs: qs
        };

        // Managed returns 202 (async job); UPS returns 204 (sync, no body)
        return this.REST.request(options, [this.HttpStatus.ACCEPTED, this.HttpStatus.NO_CONTENT], false);
    }

    /**
     * Get service bindings for a Service Instance
     * v2: {@link http://apidocs.cloudfoundry.org/226/service_instances/list_all_service_bindings_for_the_service_instance.html}
     * v3: {@link https://v3-apidocs.cloudfoundry.org/#list-service-credential-bindings}
     *
     * @param  {String} guid [Service Instance GUID]
     * @param  {Object} filter [Filter options]
     * @return {Promise} [Promise resolving to service bindings list]
     */
    getServiceBindings(guid, filter) {
        if (this.isUsingV3()) {
            return this._getServiceBindingsV3(guid, filter);
        } else {
            return this._getServiceBindingsV2(guid, filter);
        }
    }

    _getServiceBindingsV2(guid, filter) {
        const url = `${this.API_URL}/v2/service_instances/${guid}/service_bindings`;
        let qs = filter || {};

        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`
            },
            qs: qs
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    _getServiceBindingsV3(guid, filter) {
        const qs = Object.assign({}, filter || {});
        qs["service_instance_guids"] = guid;

        const url = `${this.API_URL}/v3/service_credential_bindings`;
        const options = {
            method: "GET",
            url: url,
            headers: {
                Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`,
                "Content-Type": "application/json"
            },
            qs: qs,
            json: true
        };

        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    // --- Space-scoped queries (Issue #47) ---

    /**
     * Get Service Instances filtered by Space GUID.
     *
     * @param {String} spaceGuid [Space GUID]
     * @param {Object} [filter] [Additional filter options]
     * @return {Promise} [Service instances in the space]
     */
    getInstancesBySpace(spaceGuid, filter) {
        if (this.isUsingV3()) {
            const qs = filter || {};
            qs.space_guids = spaceGuid;
            const url = `${this.API_URL}/v3/service_instances`;
            const token = `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`;
            const options = {
                method: "GET",
                url: url,
                headers: { Authorization: token, "Content-Type": "application/json" },
                qs
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        } else {
            const qs = filter || {};
            const url = `${this.API_URL}/v2/spaces/${spaceGuid}/service_instances`;
            const options = {
                method: "GET",
                url: url,
                headers: { Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}` },
                qs
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        }
    }

    /**
     * Get a Service Instance by name within a specific Space.
     *
     * @param {String} name [Service Instance name]
     * @param {String} spaceGuid [Space GUID]
     * @return {Promise} [Service instance matching name in space]
     */
    getInstanceByNameInSpace(name, spaceGuid) {
        if (this.isUsingV3()) {
            const url = `${this.API_URL}/v3/service_instances`;
            const token = `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`;
            const options = {
                method: "GET",
                url: url,
                headers: { Authorization: token, "Content-Type": "application/json" },
                qs: { names: name, space_guids: spaceGuid }
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        } else {
            const url = `${this.API_URL}/v2/spaces/${spaceGuid}/service_instances`;
            const options = {
                method: "GET",
                url: url,
                headers: { Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}` },
                qs: { q: `name:${name}` }
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        }
    }

    // --- HANA/Managed Instance lifecycle (Issue #199) ---

    /**
     * Start a managed Service Instance (e.g., HANA Cloud DB).
     * Sends PATCH with serviceStopped=false parameter.
     * Only works for managed service instances that support lifecycle operations.
     * 
     * @param {String} guid - Service instance GUID
     * @return {Promise} Resolves when start operation is accepted (202)
     */
    startInstance(guid) {
        const updateOptions = {
            parameters: {
                data: {
                    serviceStopped: false
                }
            }
        };
        return this.update(guid, updateOptions, true); // acceptsIncomplete=true
    }

    /**
     * Stop a managed Service Instance (e.g., HANA Cloud DB).
     * Sends PATCH with serviceStopped=true parameter.
     * 
     * @param {String} guid - Service instance GUID
     * @return {Promise} Resolves when stop operation is accepted (202)
     */
    stopInstance(guid) {
        const updateOptions = {
            parameters: {
                data: {
                    serviceStopped: true
                }
            }
        };
        return this.update(guid, updateOptions, true); // acceptsIncomplete=true
    }

    /**
     * Get the last operation status for a Service Instance.
     * Useful for polling async operations (accepts_incomplete).
     *
     * @param {String} guid [Service Instance GUID]
     * @return {Promise} [Resolves with last_operation object]
     */
    getOperationStatus(guid) {
        if (this.isUsingV3()) {
            const url = `${this.API_URL}/v3/service_instances/${guid}`;
            const token = `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}`;
            return this.REST.requestV3("GET", url, token).then(instance => {
                return instance.last_operation || {};
            });
        } else {
            const url = `${this.API_URL}/v2/service_instances/${guid}`;
            const options = {
                method: "GET",
                url: url,
                headers: { Authorization: `${this.UAA_TOKEN.token_type} ${this.UAA_TOKEN.access_token}` }
            };
            return this.REST.request(options, this.HttpStatus.OK, true).then(result => {
                return (result.entity && result.entity.last_operation) || {};
            });
        }
    }

    /**
     * Get ALL service instances across all pages (auto-pagination).
     * Returns a flat array of every service instance resource.
     *
     * Results are cached when caching is enabled.
     *
     * @param  {Object} [filter] Base filter (merged with pagination params)
     * @return {Promise<Array>} Flat array of all service instance resources
     */
    getAllInstances(filter) {
        const self = this;
        return this.getAllResources(function (f) { return self.getInstances(f); }, filter);
    }

}

module.exports = ServiceInstances;