MDL-81031 core: Add core/fetch to query new WS layer

This commit is contained in:
Andrew Nicols
2024-08-06 16:01:07 +08:00
committed by Jun Pataleta
parent 614b3bf9b3
commit fbca10b8f3
10 changed files with 350 additions and 19 deletions
+10
View File
@@ -0,0 +1,10 @@
define("core/fetch",["exports","core/config","./pending"],(function(_exports,_config,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* The core/fetch module allows you to make web service requests to the Moodle API.
*
* @module core/fetch
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.request=_exports.performPut=_exports.performPost=_exports.performHead=_exports.performGet=_exports.performDelete=void 0,_config=_interopRequireDefault(_config),_pending=_interopRequireDefault(_pending);const normaliseComponent=component=>component.replace(/^core_/,""),getRequest=(component,endpoint,_ref)=>{let{params:params={},body:body=null,method:method="GET"}=_ref;const url=new URL("".concat(_config.default.apibase,"rest/v2/").concat(component,"/").concat(endpoint)),options={method:method,headers:{Accept:"application/json","Content-Type":"application/json"}};return Object.entries(params).forEach((_ref2=>{let[key,value]=_ref2;url.searchParams.append(key,value)})),body&&(body instanceof FormData?options.body=body:options.body=body instanceof Object?JSON.stringify(body):body),new Request(url,options)},request=async function(component,action){let{params:params={},body:body=null,method:method="GET"}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const pending=new _pending.default("Requesting ".concat(component,"/").concat(action," with ").concat(method)),result=await fetch(getRequest(normaliseComponent(component),action,{params:params,method:method,body:body}));if(pending.resolve(),result.ok)return result.json();throw new Error(result.statusText)};_exports.request=request;_exports.performGet=function(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{params:params,method:"GET"})};_exports.performHead=function(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{params:params,method:"HEAD"})};_exports.performPost=function(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,method:"POST"})};_exports.performPut=function(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,method:"POST"})};_exports.performDelete=function(component,action){let{params:params={},body:body=null}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return request(component,action,{body:body,params:params,method:"DELETE"})}}));
//# sourceMappingURL=fetch.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
define("core/utils",["exports","core/pending"],(function(_exports,_pending){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.throttle=_exports.getNormalisedComponent=_exports.debounce=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};_exports.throttle=(func,wait)=>{let onCooldown=!1,runAgain=null;const run=function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];runAgain=null!==runAgain,onCooldown||(func.apply(this,args),onCooldown=!0,setTimeout((()=>{const recurse=runAgain;onCooldown=!1,runAgain=null,recurse&&run(args)}),wait))};return run};const debounceMap=new Map;_exports.debounce=function(func,wait){let{pending:pending=!1,cancel:cancel=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},timeout=null;const returnedFunction=function(){for(var _len2=arguments.length,args=new Array(_len2),_key2=0;_key2<_len2;_key2++)args[_key2]=arguments[_key2];pending&&!debounceMap.has(returnedFunction)&&debounceMap.set(returnedFunction,new _pending.default("core/utils:debounce")),clearTimeout(timeout),timeout=setTimeout((async()=>{const pendingPromise=debounceMap.get(returnedFunction);debounceMap.delete(returnedFunction),await func.apply(undefined,args),null==pendingPromise||pendingPromise.resolve()}),wait)};return cancel&&(returnedFunction.cancel=()=>{const pendingPromise=debounceMap.get(returnedFunction);null==pendingPromise||pendingPromise.resolve(),clearTimeout(timeout)}),returnedFunction};_exports.getNormalisedComponent=component=>component&&"moodle"!==component&&"core"!==component?component:"core"}));
define("core/utils",["exports","core/pending","jquery"],(function(_exports,_pending,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.wrapPromiseInWhenable=_exports.throttle=_exports.getNormalisedComponent=_exports.debounce=void 0,_pending=_interopRequireDefault(_pending),_jquery=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}_exports.throttle=(func,wait)=>{let onCooldown=!1,runAgain=null;const run=function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++)args[_key]=arguments[_key];runAgain=null!==runAgain,onCooldown||(func.apply(this,args),onCooldown=!0,setTimeout((()=>{const recurse=runAgain;onCooldown=!1,runAgain=null,recurse&&run(args)}),wait))};return run};const debounceMap=new Map;_exports.debounce=function(func,wait){let{pending:pending=!1,cancel:cancel=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},timeout=null;const returnedFunction=function(){for(var _len2=arguments.length,args=new Array(_len2),_key2=0;_key2<_len2;_key2++)args[_key2]=arguments[_key2];pending&&!debounceMap.has(returnedFunction)&&debounceMap.set(returnedFunction,new _pending.default("core/utils:debounce")),clearTimeout(timeout),timeout=setTimeout((async()=>{const pendingPromise=debounceMap.get(returnedFunction);debounceMap.delete(returnedFunction),await func.apply(undefined,args),null==pendingPromise||pendingPromise.resolve()}),wait)};return cancel&&(returnedFunction.cancel=()=>{const pendingPromise=debounceMap.get(returnedFunction);null==pendingPromise||pendingPromise.resolve(),clearTimeout(timeout)}),returnedFunction};_exports.getNormalisedComponent=component=>component&&"moodle"!==component&&"core"!==component?component:"core";_exports.wrapPromiseInWhenable=promise=>_jquery.default.when(promise)}));
//# sourceMappingURL=utils.min.js.map
File diff suppressed because one or more lines are too long
+237
View File
@@ -0,0 +1,237 @@
// 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 2023 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Cfg from 'core/config';
import PendingPromise from './pending';
/**
* Normalise the component name to remove the core_ prefix.
*
* @param {string} component
* @returns {string}
*/
const normaliseComponent = (component) => component.replace(/^core_/, '');
/**
* Get the Request object 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 {Request}
*/
const 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 Request(url, options);
};
/**
* 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
* @param {string} [params.method = "GET"] The HTTP method to use
* @returns {Promise<object>}
*/
const request = async(
component,
action,
{
params = {},
body = null,
method = 'GET',
} = {},
) => {
const pending = new PendingPromise(`Requesting ${component}/${action} with ${method}`);
const result = await fetch(
getRequest(
normaliseComponent(component),
action,
{params, method, body},
),
);
pending.resolve();
if (result.ok) {
return result.json();
}
throw new Error(result.statusText);
};
/**
* 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<object>}
*/
const performGet = (
component,
action,
{
params = {},
} = {},
) => 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<object>}
*/
const performHead = (
component,
action,
{
params = {},
} = {},
) => 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<object>}
*/
const performPost = (
component,
action,
{
body,
} = {},
) => 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<object>}
*/
const performPut = (
component,
action,
{
body,
} = {},
) => 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 {object} [params.params = {}] The parameters to pass to the API
* @param {string|Object|FormData} [params.body = null] The HTTP method to use
* @returns {Promise<object>}
*/
const performDelete = (
component,
action,
{
params = {},
body = null,
} = {},
) => request(
component,
action,
{
body,
params,
method: 'DELETE',
},
);
export {
request,
performGet,
performHead,
performPost,
performPut,
performDelete,
};
+9
View File
@@ -22,6 +22,7 @@
*/
import Pending from 'core/pending';
import jQuery from 'jquery';
/**
* Create a wrapper function to throttle the execution of the given
@@ -147,3 +148,11 @@ export const getNormalisedComponent = (component) => {
return 'core';
};
/**
* Wrap a Native Promise in a jQuery Whenable for b/c.
*
* @param {*} promise
* @returns {jQuery}
*/
export const wrapPromiseInWhenable = (promise) => jQuery.when(promise);
@@ -298,7 +298,7 @@ class page_requirements_manager {
* @return array List of safe config values that are available to javascript.
*/
public function get_config_for_javascript(moodle_page $page, renderer_base $renderer) {
global $CFG;
global $CFG, $USER;
if (empty($this->M_cfg)) {
$iconsystem = \core\output\icon_system::instance();
@@ -336,6 +336,7 @@ class page_requirements_manager {
'langrev' => get_string_manager()->get_revision(),
'templaterev' => $this->get_templaterev(),
'siteId' => (int) SITEID,
'userId' => (int) $USER->id,
];
if ($CFG->debugdeveloper) {
$this->M_cfg['developerdebug'] = true;
+8 -1
View File
@@ -1,3 +1,10 @@
define("core_user/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unenrolUser=_exports.submitUserEnrolmentForm=_exports.setUserPreferences=_exports.setUserPreference=_exports.sendMessagesToUsers=_exports.getUserPreferences=_exports.getUserPreference=_exports.createNotesForUsers=void 0;_exports.getUserPreference=function(name){let userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return getUserPreferences(name,userid).then((response=>response.preferences[0].value))};const getUserPreferences=function(){let name=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return(0,_ajax.call)([{methodname:"core_user_get_user_preferences",args:{name:name,userid:userid}}])[0]};_exports.getUserPreferences=getUserPreferences;_exports.setUserPreference=function(name){let value=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,userid=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return setUserPreferences([{name:name,value:value,userid:userid}])};const setUserPreferences=preferences=>(0,_ajax.call)([{methodname:"core_user_set_user_preferences",args:{preferences:preferences}}])[0];_exports.setUserPreferences=setUserPreferences;_exports.unenrolUser=userEnrolmentId=>(0,_ajax.call)([{methodname:"core_enrol_unenrol_user_enrolment",args:{ueid:userEnrolmentId}}])[0];_exports.submitUserEnrolmentForm=formdata=>(0,_ajax.call)([{methodname:"core_enrol_submit_user_enrolment_form",args:{formdata:formdata}}])[0];_exports.createNotesForUsers=notes=>(0,_ajax.call)([{methodname:"core_notes_create_notes",args:{notes:notes}}])[0];_exports.sendMessagesToUsers=messages=>(0,_ajax.call)([{methodname:"core_message_send_instant_messages",args:{messages:messages}}])[0]}));
define("core_user/repository",["exports","core/config","core/ajax","core/fetch"],(function(_exports,_config,_ajax,_fetch){var obj;
/**
* Module to handle AJAX interactions.
*
* @module core_user/repository
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unenrolUser=_exports.submitUserEnrolmentForm=_exports.setUserPreferences=_exports.setUserPreference=_exports.sendMessagesToUsers=_exports.getUserPreferences=_exports.getUserPreference=_exports.createNotesForUsers=void 0,_config=(obj=_config)&&obj.__esModule?obj:{default:obj};const checkUserId=userid=>{if(0!==Number(userid)&&Number(userid)!==_config.default.userId)throw new Error("Invalid user ID: ".concat(userid,". It is only possible to manage preferences for the current user."))},addLegacySavedProperty=(response,preferences)=>{const debugLogger={get:(target,prop,receiver)=>"then"===prop?null:"saved"===prop?(window.console.warn("The saved property is deprecated. Please use the response object directly."),preferences.filter((preference=>target.hasOwnProperty(preference.name))).map((preference=>({name:preference.name,userid:_config.default.userid})))):Reflect.get(target,prop,receiver)};return Promise.resolve(new Proxy(response,debugLogger))};_exports.getUserPreference=function(name){let userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return getUserPreferences(name,userid).then((response=>response[name]))};const getUserPreferences=function(){let name=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,userid=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;checkUserId(userid);const endpoint=["current","preferences"];return name&&endpoint.push(name),(0,_fetch.performGet)("core_user",endpoint.join("/"))};_exports.getUserPreferences=getUserPreferences;_exports.setUserPreference=function(name){let value=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,userid=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return checkUserId(userid),(0,_fetch.performPost)("core_user","current/preferences/".concat(name),{body:{value:value}}).then((response=>addLegacySavedProperty(response,[{name:name}])))};_exports.setUserPreferences=preferences=>(preferences.forEach((preference=>checkUserId(preference.userid))),(0,_fetch.performPost)("core_user","current/preferences",{body:{preferences:Object.fromEntries(preferences.map((preference=>[preference.name,preference.value])))}}).then((response=>addLegacySavedProperty(response,preferences))));_exports.unenrolUser=userEnrolmentId=>(0,_ajax.call)([{methodname:"core_enrol_unenrol_user_enrolment",args:{ueid:userEnrolmentId}}])[0];_exports.submitUserEnrolmentForm=formdata=>(0,_ajax.call)([{methodname:"core_enrol_submit_user_enrolment_form",args:{formdata:formdata}}])[0];_exports.createNotesForUsers=notes=>(0,_ajax.call)([{methodname:"core_notes_create_notes",args:{notes:notes}}])[0];_exports.sendMessagesToUsers=messages=>(0,_ajax.call)([{methodname:"core_message_send_instant_messages",args:{messages:messages}}])[0]}));
//# sourceMappingURL=repository.min.js.map
File diff suppressed because one or more lines are too long
+80 -14
View File
@@ -21,7 +21,54 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Config from 'core/config';
import {call as fetchMany} from 'core/ajax';
import {performGet, performPost} from 'core/fetch';
const checkUserId = (userid) => {
if (Number(userid) === 0) {
return;
}
if (Number(userid) === Config.userId) {
return;
}
throw new Error(
`Invalid user ID: ${userid}. It is only possible to manage preferences for the current user.`,
);
};
/**
* Turn the response object into a Proxy object that will log a warning if the saved property is accessed.
*
* @param {Object} response
* @param {Object} preferences The preferences that might be in the response
* @return {Promise<Proxy>}
*/
const addLegacySavedProperty = (response, preferences) => {
const debugLogger = {
get(target, prop, receiver) {
if (prop === 'then') {
// To proxy a Promise we have to return null when the then key is requested.
return null;
}
if (prop === 'saved') {
window.console.warn(
'The saved property is deprecated. Please use the response object directly.',
);
return preferences
.filter((preference) => target.hasOwnProperty(preference.name))
.map((preference) => ({
name: preference.name,
userid: Config.userid,
}));
}
return Reflect.get(target, prop, receiver);
},
};
return Promise.resolve(new Proxy(response, debugLogger));
};
/**
* Get single user preference
@@ -30,23 +77,25 @@ import {call as fetchMany} from 'core/ajax';
* @param {Number} userid User ID (defaults to current user)
* @return {Promise}
*/
export const getUserPreference = (name, userid = 0) => {
return getUserPreferences(name, userid)
.then(response => response.preferences[0].value);
};
export const getUserPreference = (name, userid = 0) => getUserPreferences(name, userid)
.then((response) => response[name]);
/**
* Get multiple user preferences
*
* @param {String|null} name Name of the preference (omit if you want to retrieve all)
* @param {Number} userid User ID (defaults to current user)
* @return {Promise}
* @return {Promise<object<string, string>>}
*/
export const getUserPreferences = (name = null, userid = 0) => {
return fetchMany([{
methodname: 'core_user_get_user_preferences',
args: {name, userid}
}])[0];
checkUserId(userid);
const endpoint = ['current', 'preferences'];
if (name) {
endpoint.push(name);
}
return performGet('core_user', endpoint.join('/'));
};
/**
@@ -58,7 +107,16 @@ export const getUserPreferences = (name = null, userid = 0) => {
* @return {Promise}
*/
export const setUserPreference = (name, value = null, userid = 0) => {
return setUserPreferences([{name, value, userid}]);
checkUserId(userid);
return performPost(
'core_user',
`current/preferences/${name}`,
{
body: {value},
},
)
// Return the result of the fetch call, and also add in the legacy saved property.
.then((response) => addLegacySavedProperty(response, [{name}]));
};
/**
@@ -68,10 +126,18 @@ export const setUserPreference = (name, value = null, userid = 0) => {
* @return {Promise}
*/
export const setUserPreferences = (preferences) => {
return fetchMany([{
methodname: 'core_user_set_user_preferences',
args: {preferences}
}])[0];
preferences.forEach((preference) => checkUserId(preference.userid));
return performPost(
'core_user',
'current/preferences',
{
body: {
preferences: Object.fromEntries (preferences.map((preference) => ([preference.name, preference.value]))),
},
},
)
// Return the result of the fetch call, and also add in the legacy saved property.
.then((response) => addLegacySavedProperty(response, preferences));
};
/**