// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The core/fetch module allows you to make web service requests to the Moodle API.
*
* @module core/fetch
* @copyright Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @example <caption>Perform a single GET request</caption>
* import Fetch from 'core/fetch';
*
* const result = Fetch.performGet('mod_example', 'animals', { params: { type: 'mammal' } });
*
* result.then((response) => {
* // Do something with the Response object.
* })
* .catch((error) => {
* // Handle the error
* });
*/
import Cfg from 'core/config';
import PendingPromise from './pending';
/**
* A wrapper around the Request, including a Promise that is resolved when the request is complete.
*
* @class RequestWrapper
* @private
*/
class RequestWrapper {
/** @var {Request} */
#request = null;
/** @var {Promise} */
#promise = null;
/** @var {Function} */
#resolve = null;
/** @var {Function} */
#reject = null;
/**
* Create a new RequestWrapper.
*
* @param {Request} request The request object that is wrapped
*/
constructor(request) {
this.#request = request;
this.#promise = new Promise((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
});
}
/**
* Get the wrapped Request.
*
* @returns {Request}
* @private
*/
get request() {
return this.#request;
}
/**
* Get the Promise link to this request.
*
* @return {Promise}
* @private
*/
get promise() {
return this.#promise;
}
/**
* Handle the response from the request.
*
* @param {Response} response
* @private
*/
handleResponse(response) {
if (response.ok) {
this.#resolve(response);
} else {
this.#reject(response.statusText);
}
}
}
/**
* A class to handle requests to the Moodle REST API.
*
* @class Fetch
*/
export default class Fetch {
/**
* Make a single request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @param {string} [params.method = "GET"] The HTTP method to use
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static async request(
component,
action,
{
params = {},
body = null,
method = 'GET',
} = {},
) {
const pending = new PendingPromise(`Requesting ${component}/${action} with ${method}`);
const requestWrapper = Fetch.#getRequest(
Fetch.#normaliseComponent(component),
action,
{ params, method, body },
);
const result = await fetch(requestWrapper.request);
pending.resolve();
requestWrapper.handleResponse(result);
return requestWrapper.promise;
}
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performGet(
component,
action,
{
params = {},
} = {},
) {
return this.request(
component,
action,
{ params, method: 'GET' },
);
}
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performHead(
component,
action,
{
params = {},
} = {},
) {
return this.request(
component,
action,
{ params, method: 'HEAD' },
);
}
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {string|Object|FormData} params.body The HTTP method to use
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performPost(
component,
action,
{
body,
} = {},
) {
return this.request(
component,
action,
{ body, method: 'POST' },
);
}
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {string|Object|FormData} params.body The HTTP method to use
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performPut(
component,
action,
{
body,
} = {},
) {
return this.request(
component,
action,
{ body, method: 'PUT' },
);
}
/**
* Make a PATCH request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {string|Object|FormData} params.body The HTTP method to use
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performPatch(
component,
action,
{
body,
} = {},
) {
return this.request(
component,
action,
{ body, method: 'PATCH' },
);
}
/**
* Make a request to the Moodle API.
*
* @param {string} component The frankenstyle component name
* @param {string} action The component action to perform
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @returns {Promise<Response>} A promise that resolves to the Response object for the request
*/
static performDelete(
component,
action,
{
params = {},
body = null,
} = {},
) {
return this.request(
component,
action,
{
body,
params,
method: 'DELETE',
},
);
}
/**
* Normalise the component name to remove the core_ prefix.
*
* @param {string} component
* @returns {string}
*/
static #normaliseComponent(component) {
return component.replace(/^core_/, '');
}
/**
* Get the Request for a given API request.
*
* @param {string} component The frankenstyle component name
* @param {string} endpoint The endpoint within the componet to call
* @param {object} params
* @param {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @param {string} [params.method = "GET"] The HTTP method to use
* @returns {RequestWrapper}
*/
static #getRequest(
component,
endpoint,
{
params = {},
body = null,
method = 'GET',
}
) {
const url = new URL(`${Cfg.apibase}/rest/v2/${component}/${endpoint}`);
const options = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
};
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
if (body) {
if (body instanceof FormData) {
options.body = body;
} else if (body instanceof Object) {
options.body = JSON.stringify(body);
} else {
options.body = body;
}
}
return new RequestWrapper(new Request(url, options));
}
}