diff --git a/Client/client.js b/Client/client.js index 211b0fa87..9f4a241e0 100644 --- a/Client/client.js +++ b/Client/client.js @@ -81,6 +81,7 @@ try { Setting up external dependencies. */ CL.nodeModules = { + fs: require('fs'), nodeFetch: require('node-fetch') } diff --git a/Client/githubServer.js b/Client/githubServer.js index 9bec1d167..ab6c1db49 100644 --- a/Client/githubServer.js +++ b/Client/githubServer.js @@ -103,12 +103,12 @@ exports.newGithubServer = function newGithubServer() { page }); - if (listResponse.data.length < 100) { + if (filesChanged.length < 100) { lastPage = true } - for (let i = 0; i < listResponse.data.length; i++) { - let listItem = listResponse.data[i] + for (let i = 0; i < filesChanged.length; i++) { + let listItem = filesChanged[i] let githubUsername switch (endpoint) { case 'activity': { @@ -125,7 +125,7 @@ exports.newGithubServer = function newGithubServer() { githubListArray.push(githubUsername) } console.log('[INFO] Github Server -> getRepoInfo -> doGithub -> getList -> ' + method + ' Page = ' + page) - console.log('[INFO] Github Server -> getRepoInfo -> doGithub -> getList -> ' + method + ' Received = ' + listResponse.data.length) + console.log('[INFO] Github Server -> getRepoInfo -> doGithub -> getList -> ' + method + ' Received = ' + filesChanged.length) } catch (err) { console.log(err) @@ -237,16 +237,16 @@ exports.newGithubServer = function newGithubServer() { page: page }); - if (listResponse.data.length < 100) { + if (filesChanged.length < 100) { lastPage = true } console.log('[INFO] Github Server -> mergeGithubPullRequests -> doGithub -> getPrList -> Receiving Page = ' + page) - for (let i = 0; i < listResponse.data.length; i++) { - let pullRequest = listResponse.data[i] + for (let i = 0; i < filesChanged.length; i++) { + let pullRequest = filesChanged[i] console.log('[INFO] Github Server -> mergeGithubPullRequests -> doGithub -> getPrList -> Pull Request "' + pullRequest.title + '" found and added to the list to validate. ') githubPrListArray.push(pullRequest) } - console.log('[INFO] Github Server -> mergeGithubPullRequests -> doGithub -> getPrList -> Received = ' + listResponse.data.length) + console.log('[INFO] Github Server -> mergeGithubPullRequests -> doGithub -> getPrList -> Received = ' + filesChanged.length) } catch (err) { console.log(err) @@ -289,153 +289,20 @@ exports.newGithubServer = function newGithubServer() { repo: repo, pull_number: pullRequest.number }); - /* - Validation #1: If the PR has more than one file, then we will not automatically - merge it. - */ - if (listResponse.data.length !== 1) { - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #1 Failed -> Pull Request "' + pullRequest.title + '" not merged because it contains more than 1 file. -> fileCount = ' + listResponse.data.length) - /* - We will close PRs that contains any User Profile file together with other files in the same Pull Request. - This will avoid manual merges to include User Profile files. - */ - for (j = 0; j < listResponse.data.length; j++) { - let pullRequestFile = listResponse.data[j] - let fileContentUrl = pullRequestFile.raw_url - if (fileContentUrl.indexOf('Governance/Plugins/User-Profiles') >= 0) { - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: pullRequest.number, - body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that a User Profile file... \n\n' + fileContentUrl + '\n\n...was submitted together with ' + (listResponse.data.length - 1) + ' other file/s. User Profiles files as per the Governance System rules, must be the only file present at a Pull Request in order to pass all the validations and be automatically merged.' - }); + let filesChanged = listResponse.data + let fileContentUrl // URL to the only file at the PR + let fileContent // File content of the only file at the PR + let userProfile // User Profile Object - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.pulls.update({ - owner: owner, - repo: repo, - pull_number: pullRequest.number, - state: 'closed' - }); - break - } - } - continue - } - let pullRequestFile = listResponse.data[0] - /* - Validation #2: File Name must be the same to the Github Username of the PR owner. - */ - let fileContentUrl = pullRequestFile.raw_url + if (await validatePrHasMoreThanOneFile() === false) { continue } + if (await validateFileNameEqualsGithubUsername() === false) { continue } + if (await validateFileChangedIsUserProfile() === false) { continue } + if (await validateMessageSignedIsGithubUsername() === false) { continue } + if (await validateUserProfileNodeNameEqualsGithubUsername() === false) { continue } + if (await validateUserProfileIdDoesNotBelongtoAnotherUserProfile() === false) { continue } - if (fileContentUrl.indexOf('Governance/Plugins/User-Profiles') < 0) { - /* - If it is not a user profile then there is no need to auto merge. - */ - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #2 Failed -> Pull Request "' + pullRequest.title + '" not merged because the file modified at the Pull Request is not a User Profile file. -> fileContentUrl = ' + fileContentUrl) - continue - } - - let splittedURL = fileContentUrl.split('/') - let fileName = splittedURL[splittedURL.length - 1] - let splittedFileName = fileName.split('.') - fileName = splittedFileName[0] - let githubUsername = pullRequest.user.login - - if (githubUsername !== fileName) { - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #2 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the File Name. -> Github Username = ' + githubUsername + '-> fileName = ' + fileName) - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: pullRequest.number, - body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the name of the User Profile Plugin File.\n\n File Name = "' + fileName + '"' - }); - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.pulls.update({ - owner: owner, - repo: repo, - pull_number: pullRequest.number, - state: 'closed' - }); - continue - } - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - let fileContent = await CL.projects.foundations.utilities.webAccess.fetchAPIDataFile(fileContentUrl) - - let userProfile = JSON.parse(fileContent) - /* - Validation #3: The file changed at the PR is a User Profile. - */ - if (userProfile.type !== 'User Profile') { - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #3 Failed -> Pull Request "' + pullRequest.title + '" not merged because the file modified is not a User Profile. -> Type = ' + userProfile.type) - continue - } - /* - Validation #4: The message signed at the config is not the Github Username. - */ - let config = JSON.parse(userProfile.config) - let messageSigned = config.signature.message - if (messageSigned !== githubUsername) { - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #4 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the Message Signed at the User Profile. -> Github Username = ' + githubUsername + '-> messageSigned = ' + messageSigned) - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: pullRequest.number, - body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the Message Signed at the User Profile.\n\n Message Signed = "' + messageSigned + '"' - }); - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.pulls.update({ - owner: owner, - repo: repo, - pull_number: pullRequest.number, - state: 'closed' - }); - continue - } - /* - Validation #5: The name of the User Profile node is not the Github Username. - */ - if (userProfile.name !== githubUsername) { - console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #5 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the User Profile node\'s name. -> Github Username = ' + githubUsername + '-> userProfile.name = ' + userProfile.name) - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: pullRequest.number, - body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the User Profile node\'s name.\n\n User Profile Name = "' + userProfile.name + '"' - }); - - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - await octokit.rest.pulls.update({ - owner: owner, - repo: repo, - pull_number: pullRequest.number, - state: 'closed' - }); - continue - } - /* - All validations passed, we will proceed an merge this Pull Request. - */ - await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) - let mergeResponse = await octokit.rest.pulls.merge({ - owner: owner, - repo: repo, - pull_number: pullRequest.number, - commit_title: 'Changes in User Profile done by Profile Owner ' + githubUsername + ' automatically merged by Superalgos.' - }); - - if (mergeResponse.data.merged !== true) { + if (await mergePullRequest() === false) { console.log('[WARN] Github Server -> mergeGithubPullRequests -> Merge Failed -> Pull Request "' + pullRequest.title + '" not merged because Github could not merge it. -> mergeResponse.message = ' + mergeResponse.data.message) - continue } else { console.log('[INFO] Github Server -> mergeGithubPullRequests -> Merge Succed -> Pull Request "' + pullRequest.title + '" successfully merged. -> mergeResponse.message = ' + mergeResponse.data.message) await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) @@ -445,7 +312,216 @@ exports.newGithubServer = function newGithubServer() { issue_number: pullRequest.number, body: 'This Pull Request was automatically merged by the Superalgos Governance System because it was detected that the Github User " ' + githubUsername + '" who submitted it, modified its own User Profile Plugin File and nothning else but that file. All validations were successfull.' }); - continue + } + + async function validatePrHasMoreThanOneFile() { + /* + Validation #1: If the PR has more than one file, then we will not automatically + merge it. + */ + if (filesChanged.length !== 1) { + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #1 Failed -> Pull Request "' + pullRequest.title + '" not merged because it contains more than 1 file. -> fileCount = ' + filesChanged.length) + /* + We will close PRs that contains any User Profile file together with other files in the same Pull Request. + This will avoid manual merges to include User Profile files. + */ + for (j = 0; j < filesChanged.length; j++) { + let pullRequestFile = filesChanged[j] + let fileContentUrl = pullRequestFile.raw_url + if (fileContentUrl.indexOf('Governance/Plugins/User-Profiles') >= 0) { + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: pullRequest.number, + body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that a User Profile file... \n\n' + fileContentUrl + '\n\n...was submitted together with ' + (filesChanged.length - 1) + ' other file/s. User Profiles files as per the Governance System rules, must be the only file present at a Pull Request in order to pass all the validations and be automatically merged.' + }); + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.pulls.update({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + state: 'closed' + }); + break + } + } + return false + } + } + + async function validateFileNameEqualsGithubUsername() { + /* + Validation #2: File Name must be the same to the Github Username of the PR owner. + */ + let pullRequestFile = filesChanged[0] + fileContentUrl = pullRequestFile.raw_url + + if (fileContentUrl.indexOf('Governance/Plugins/User-Profiles') < 0) { + /* + If it is not a user profile then there is no need to auto merge. + */ + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #2 Failed -> Pull Request "' + pullRequest.title + '" not merged because the file modified at the Pull Request is not a User Profile file. -> fileContentUrl = ' + fileContentUrl) + return false + } + + let splittedURL = fileContentUrl.split('/') + let fileName = splittedURL[splittedURL.length - 1] + let splittedFileName = fileName.split('.') + fileName = splittedFileName[0] + let githubUsername = pullRequest.user.login + + if (githubUsername !== fileName) { + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #2 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the File Name. -> Github Username = ' + githubUsername + '-> fileName = ' + fileName) + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: pullRequest.number, + body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the name of the User Profile Plugin File.\n\n File Name = "' + fileName + '"' + }); + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.pulls.update({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + state: 'closed' + }); + return false + } + } + + async function validateFileChangedIsUserProfile() { + /* + Validation #3: The file changed at the PR is a User Profile. + */ + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + fileContent = await CL.projects.foundations.utilities.webAccess.fetchAPIDataFile(fileContentUrl) + userProfile = JSON.parse(fileContent) + + if (userProfile.type !== 'User Profile') { + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #3 Failed -> Pull Request "' + pullRequest.title + '" not merged because the file modified is not a User Profile. -> Type = ' + userProfile.type) + return false + } + } + + async function validateMessageSignedIsGithubUsername() { + /* + Validation #4: The message signed at the config is not the Github Username. + */ + let config = JSON.parse(userProfile.config) + let messageSigned = config.signature.message + if (messageSigned !== githubUsername) { + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #4 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the Message Signed at the User Profile. -> Github Username = ' + githubUsername + '-> messageSigned = ' + messageSigned) + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: pullRequest.number, + body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the Message Signed at the User Profile.\n\n Message Signed = "' + messageSigned + '"' + }); + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.pulls.update({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + state: 'closed' + }); + return false + } + } + + async function validateUserProfileNodeNameEqualsGithubUsername() { + /* + Validation #5: The name of the User Profile node is not the Github Username. + */ + if (userProfile.name !== githubUsername) { + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #5 Failed -> Pull Request "' + pullRequest.title + '" not merged because the Github Username is not equal to the User Profile node\'s name. -> Github Username = ' + githubUsername + '-> userProfile.name = ' + userProfile.name) + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: pullRequest.number, + body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because it was detected that the Github User "' + githubUsername + '" who submitted it, is not equal to the User Profile node\'s name.\n\n User Profile Name = "' + userProfile.name + '"' + }); + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.pulls.update({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + state: 'closed' + }); + return false + } + } + + async function validateUserProfileIdDoesNotBelongtoAnotherUserProfile() { + /* + Validation #6: The id of the User Profile can not be the id of an already existing User Profile + unless the existing one belongs to the same Github username. + */ + let userProfileIdMap = new Map() + let pluginFileNames = await CL.projects.foundations.utilities.plugins.getPluginFileNames( + 'Governance', + 'User-Profiles' + ) + for (let i = 0; i < pluginFileNames.length; i++) { + let pluginFileName = pluginFileNames[i] + + let pluginFileContent = await CL.projects.foundations.utilities.plugins.getPluginFileContent( + 'Governance', + 'User-Profiles', + pluginFileName + ) + + let otherUserProfile = JSON.parse(pluginFileContent) + userProfileIdMap.set(otherUserProfile.id, otherUserProfile.name) + } + + let testUserProfile = userProfileIdMap.get(userProfile.id) + if (testUserProfile === undefined) { return true } + if (testUserProfile !== userProfile.name) { + + console.log('[INFO] Github Server -> mergeGithubPullRequests -> Validation #6 Failed -> Pull Request "' + pullRequest.title + '" not merged because the User Profile Id already exists and belongs to another User Profile on record. -> Profile Id = ' + userProfile.id + '-> User Profile with the same Id = ' + testUserProfile) + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: pullRequest.number, + body: 'This Pull Request could not be automatically merged and was closed by the Superalgos Governance System because the User Profile Id already exists and belongs to another User Profile on record. \n\nUser Profile Id = "' + userProfile.id + '" \n\n User Profile with the same Id = "' + testUserProfile + '"' + }); + + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + await octokit.rest.pulls.update({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + state: 'closed' + }); + return false + } + } + + async function mergePullRequest() { + /* + All validations passed, we will proceed an merge this Pull Request. + */ + await CL.projects.foundations.utilities.asyncFunctions.sleep(GITHUB_API_WAITING_TIME) + let mergeResponse = await octokit.rest.pulls.merge({ + owner: owner, + repo: repo, + pull_number: pullRequest.number, + commit_title: 'Changes in User Profile done by Profile Owner ' + githubUsername + ' automatically merged by Superalgos.' + }); + return mergeResponse.data.merged } } } diff --git a/Client/httpInterface.js b/Client/httpInterface.js index 58bb8e5fd..c5d3ed75c 100644 --- a/Client/httpInterface.js +++ b/Client/httpInterface.js @@ -1,13 +1,13 @@ exports.newHttpInterface = function newHttpInterface( - WEB_SERVER, - DATA_FILE_SERVER, - PROJECT_FILE_SERVER, - UI_FILE_SERVER, - PLUGIN_SERVER, - CCXT_SERVER, + WEB_SERVER, + DATA_FILE_SERVER, + PROJECT_FILE_SERVER, + UI_FILE_SERVER, + PLUGIN_SERVER, + CCXT_SERVER, WEB3_SERVER, GITHUB_SERVER - ) { +) { /* IMPORTANT: If you are reviewing the code of the project please note @@ -1118,6 +1118,7 @@ exports.newHttpInterface = function newHttpInterface( } break } + case 'LegacyPlotter.js': { respondWithFile(global.env.PATH_TO_CLIENT + 'WebServer/LegacyPlotter.js', httpResponse) @@ -1332,30 +1333,70 @@ exports.newHttpInterface = function newHttpInterface( case 'PluginFileNames': { - let project = unescape(requestParameters[2]) - let pluginType = unescape(requestParameters[3]) + processRequest() - const fs = require('fs') - let folder = global.env.PATH_TO_PROJECTS + '/' + project + '/Plugins/' + pluginType + async function processRequest(body) { + try { + let project = unescape(requestParameters[2]) + let folder = unescape(requestParameters[3]) - fs.readdir(folder, (err, files) => { - if (files === undefined) { - files = [] + let response = await CL.projects.foundations.utilities.plugins.getPluginFileNames( + project, + folder + ) + + respondWithContent(JSON.stringify(response), httpResponse) + + } catch (err) { + console.log('[ERROR] httpInterface -> PluginFileNames -> Method call produced an error.') + console.log('[ERROR] httpInterface -> PluginFileNames -> err.stack = ' + err.stack) + console.log('[ERROR] httpInterface -> PluginFileNames -> Params Received = ' + body) + + let error = { + result: 'Fail Because', + message: err.message, + stack: err.stack + } + respondWithContent(JSON.stringify(error), httpResponse) } - respondWithContent(JSON.stringify(files), httpResponse) - }) + } + break } - break case 'LoadPlugin': + { - let project = unescape(requestParameters[2]) - let pluginType = unescape(requestParameters[3]) - let fileName = unescape(requestParameters[4]) - let filePath = global.env.PATH_TO_PROJECTS + '/' + project + '/Plugins/' + pluginType + '/' + fileName - respondWithFile(filePath, httpResponse) + processRequest() + + async function processRequest(body) { + try { + let project = unescape(requestParameters[2]) + let folder = unescape(requestParameters[3]) + let fileName = unescape(requestParameters[4]) + + let response = await CL.projects.foundations.utilities.plugins.getPluginFileContent( + project, + folder, + fileName + ) + + respondWithContent(response, httpResponse) + + } catch (err) { + console.log('[ERROR] httpInterface -> LoadPlugin -> Method call produced an error.') + console.log('[ERROR] httpInterface -> LoadPlugin -> err.stack = ' + err.stack) + console.log('[ERROR] httpInterface -> LoadPlugin -> Params Received = ' + body) + + let error = { + result: 'Fail Because', + message: err.message, + stack: err.stack + } + respondWithContent(JSON.stringify(error), httpResponse) + } + } + break } - break case 'SavePlugin': getBody(processRequest) diff --git a/Projects/Foundations/CL/Utilities/Plugins.js b/Projects/Foundations/CL/Utilities/Plugins.js new file mode 100644 index 000000000..4f87397ea --- /dev/null +++ b/Projects/Foundations/CL/Utilities/Plugins.js @@ -0,0 +1,43 @@ +exports.newFoundationsUtilitiesPlugins = function () { + + let thisObject = { + getPluginFileNames: getPluginFileNames, + getPluginFileContent: getPluginFileContent + } + + return thisObject + + async function getPluginFileNames(project, folder) { + let promise = new Promise((resolve, reject) => { + + let path = global.env.PATH_TO_PROJECTS + '/' + project + '/Plugins/' + folder + + CL.nodeModules.fs.readdir(path, (err, files) => { + if (err) { reject() } + if (files === undefined) { + files = [] + } + resolve(files) + }) + + } + ) + return promise + } + + async function getPluginFileContent(project, folder, fileName) { + let promise = new Promise((resolve, reject) => { + + let path = global.env.PATH_TO_PROJECTS + '/' + project + '/Plugins/' + folder + '/' + fileName + + CL.nodeModules.fs.readFile(path, onFileRead) + + function onFileRead(err, file) { + if (err) { reject() } + resolve(file.toString()) + } + } + ) + return promise + } +} \ No newline at end of file diff --git a/Projects/ProjectsSchema.json b/Projects/ProjectsSchema.json index 15d0d56ba..69eb196d4 100644 --- a/Projects/ProjectsSchema.json +++ b/Projects/ProjectsSchema.json @@ -884,6 +884,12 @@ "propertyName": "webAccess", "functionName": "newFoundationsUtilitiesWebAccess", "fileName": "WebAccess.js" + }, + { + "name": "Plugins", + "propertyName": "plugins", + "functionName": "newFoundationsUtilitiesPlugins", + "fileName": "Plugins.js" } ] },