http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/clusters.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/clusters.js b/modules/web-console/backend/services/clusters.js new file mode 100644 index 0000000..6c2722b --- /dev/null +++ b/modules/web-console/backend/services/clusters.js @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/clusters', + inject: ['require(lodash)', 'mongo', 'services/spaces', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param {SpacesService} spacesService + * @param errors + * @returns {ClustersService} + */ +module.exports.factory = (_, mongo, spacesService, errors) => { + /** + * Convert remove status operation to own presentation. + * @param {RemoveResult} result - The results of remove operation. + */ + const convertRemoveStatus = ({result}) => ({rowsAffected: result.n}); + + /** + * Update existing cluster + * @param {Object} cluster - The cluster for updating + * @returns {Promise.<mongo.ObjectId>} that resolves cluster id + */ + const update = (cluster) => { + const clusterId = cluster._id; + + return mongo.Cluster.update({_id: clusterId}, cluster, {upsert: true}).exec() + .then(() => mongo.Cache.update({_id: {$in: cluster.caches}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec()) + .then(() => mongo.Cache.update({_id: {$nin: cluster.caches}}, {$pull: {clusters: clusterId}}, {multi: true}).exec()) + .then(() => mongo.Igfs.update({_id: {$in: cluster.igfss}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec()) + .then(() => mongo.Igfs.update({_id: {$nin: cluster.igfss}}, {$pull: {clusters: clusterId}}, {multi: true}).exec()) + .then(() => cluster) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Cluster with name: "' + cluster.name + '" already exist.'); + }); + }; + + /** + * Create new cluster. + * @param {Object} cluster - The cluster for creation. + * @returns {Promise.<mongo.ObjectId>} that resolves cluster id. + */ + const create = (cluster) => { + return mongo.Cluster.create(cluster) + .then((savedCluster) => + mongo.Cache.update({_id: {$in: savedCluster.caches}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec() + .then(() => mongo.Igfs.update({_id: {$in: savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec()) + .then(() => savedCluster) + ) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Cluster with name: "' + cluster.name + '" already exist.'); + }); + }; + + /** + * Remove all caches by space ids. + * @param {Number[]} spaceIds - The space ids for cache deletion. + * @returns {Promise.<RemoveResult>} - that resolves results of remove operation. + */ + const removeAllBySpaces = (spaceIds) => { + return mongo.Cache.update({space: {$in: spaceIds}}, {clusters: []}, {multi: true}).exec() + .then(() => mongo.Igfs.update({space: {$in: spaceIds}}, {clusters: []}, {multi: true}).exec()) + .then(() => mongo.Cluster.remove({space: {$in: spaceIds}}).exec()); + }; + + class ClustersService { + /** + * Create or update cluster. + * @param {Object} cluster - The cluster + * @returns {Promise.<mongo.ObjectId>} that resolves cluster id of merge operation. + */ + static merge(cluster) { + if (cluster._id) + return update(cluster); + + return create(cluster); + } + + /** + * Get clusters and linked objects by space. + * @param {mongo.ObjectId|String} spaceIds - The spaces id that own cluster. + * @returns {Promise.<[mongo.Cache[], mongo.Cluster[], mongo.DomainModel[], mongo.Space[]]>} - contains requested caches and array of linked objects: clusters, domains, spaces. + */ + static listBySpaces(spaceIds) { + return mongo.Cluster.find({space: {$in: spaceIds}}).sort('name').lean().exec(); + } + + /** + * Remove cluster. + * @param {mongo.ObjectId|String} clusterId - The cluster id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(clusterId) { + if (_.isNil(clusterId)) + return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null')); + + return mongo.Cache.update({clusters: {$in: [clusterId]}}, {$pull: {clusters: clusterId}}, {multi: true}).exec() + .then(() => mongo.Igfs.update({clusters: {$in: [clusterId]}}, {$pull: {clusters: clusterId}}, {multi: true}).exec()) + .then(() => mongo.Cluster.remove({_id: clusterId}).exec()) + .then(convertRemoveStatus); + } + + /** + * Remove all clusters by user. + * @param {mongo.ObjectId|String} userId - The user id that own cluster. + * @param {Boolean} demo - The flag indicates that need lookup in demo space. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static removeAll(userId, demo) { + return spacesService.spaceIds(userId, demo) + .then(removeAllBySpaces) + .then(convertRemoveStatus); + } + } + + return ClustersService; +};
http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/configurations.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/configurations.js b/modules/web-console/backend/services/configurations.js new file mode 100644 index 0000000..7eef8a2 --- /dev/null +++ b/modules/web-console/backend/services/configurations.js @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/configurations', + inject: ['require(lodash)', 'mongo', 'services/spaces', 'services/clusters', 'services/caches', 'services/domains', 'services/igfss'] +}; + +/** + * @param _ + * @param mongo + * @param {SpacesService} spacesService + * @param {ClustersService} clustersService + * @param {CachesService} cachesService + * @param {DomainsService} domainsService + * @param {IgfssService} igfssService + * @returns {ConfigurationsService} + */ +module.exports.factory = (_, mongo, spacesService, clustersService, cachesService, domainsService, igfssService) => { + class ConfigurationsService { + static list(userId, demo) { + let spaces; + + return spacesService.spaces(userId, demo) + .then((_spaces) => { + spaces = _spaces; + + return spaces.map((space) => space._id); + }) + .then((spaceIds) => Promise.all([ + clustersService.listBySpaces(spaceIds), + domainsService.listBySpaces(spaceIds), + cachesService.listBySpaces(spaceIds), + igfssService.listBySpaces(spaceIds) + ])) + .then(([clusters, domains, caches, igfss]) => ({clusters, domains, caches, igfss, spaces})); + } + } + + return ConfigurationsService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/domains.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/domains.js b/modules/web-console/backend/services/domains.js new file mode 100644 index 0000000..3e4e129 --- /dev/null +++ b/modules/web-console/backend/services/domains.js @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/domains', + inject: ['require(lodash)', 'mongo', 'services/spaces', 'services/caches', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param {SpacesService} spacesService + * @param {CachesService} cachesService + * @param errors + * @returns {DomainsService} + */ +module.exports.factory = (_, mongo, spacesService, cachesService, errors) => { + /** + * Convert remove status operation to own presentation. + * @param {RemoveResult} result - The results of remove operation. + */ + const convertRemoveStatus = ({result}) => ({rowsAffected: result.n}); + + const _updateCacheStore = (cacheStoreChanges) => + Promise.all(_.map(cacheStoreChanges, (change) => mongo.Cache.update({_id: {$eq: change.cacheId}}, change.change, {}).exec())); + + /** + * Update existing domain + * @param {Object} domain - The domain for updating + * @param savedDomains List of saved domains. + * @returns {Promise.<mongo.ObjectId>} that resolves domain id + */ + const update = (domain, savedDomains) => { + const domainId = domain._id; + + return mongo.DomainModel.update({_id: domainId}, domain, {upsert: true}).exec() + .then(() => mongo.Cache.update({_id: {$in: domain.caches}}, {$addToSet: {domains: domainId}}, {multi: true}).exec()) + .then(() => mongo.Cache.update({_id: {$nin: domain.caches}}, {$pull: {domains: domainId}}, {multi: true}).exec()) + .then(() => { + savedDomains.push(domain); + + return _updateCacheStore(domain.cacheStoreChanges); + }) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Domain model with value type: "' + domain.valueType + '" already exist.'); + }); + }; + + /** + * Create new domain. + * @param {Object} domain - The domain for creation. + * @param savedDomains List of saved domains. + * @returns {Promise.<mongo.ObjectId>} that resolves cluster id. + */ + const create = (domain, savedDomains) => { + return mongo.DomainModel.create(domain) + .then((createdDomain) => { + savedDomains.push(createdDomain); + + return mongo.Cache.update({_id: {$in: domain.caches}}, {$addToSet: {domains: createdDomain._id}}, {multi: true}).exec() + .then(() => _updateCacheStore(domain.cacheStoreChanges)); + }) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Domain model with value type: "' + domain.valueType + '" already exist.'); + }); + }; + + const _saveDomainModel = (domain, savedDomains) => { + const domainId = domain._id; + + if (domainId) + return update(domain, savedDomains); + + return create(domain, savedDomains); + }; + + const _save = (domains) => { + if (_.isEmpty(domains)) + throw new errors.IllegalArgumentException('Nothing to save!'); + + const savedDomains = []; + const generatedCaches = []; + + const promises = _.map(domains, (domain) => { + if (domain.newCache) { + return mongo.Cache.findOne({space: domain.space, name: domain.newCache.name}).exec() + .then((cache) => { + if (cache) + return Promise.resolve(cache); + + // If cache not found, then create it and associate with domain model. + const newCache = domain.newCache; + newCache.space = domain.space; + + return cachesService.merge(newCache); + }) + .then((cache) => { + domain.caches = [cache._id]; + + return _saveDomainModel(domain, savedDomains); + }); + } + + return _saveDomainModel(domain, savedDomains); + }); + + return Promise.all(promises).then(() => ({savedDomains, generatedCaches})); + }; + + /** + * Remove all caches by space ids. + * @param {Array.<Number>} spaceIds - The space ids for cache deletion. + * @returns {Promise.<RemoveResult>} - that resolves results of remove operation. + */ + const removeAllBySpaces = (spaceIds) => { + return mongo.Cache.update({space: {$in: spaceIds}}, {domains: []}, {multi: true}).exec() + .then(() => mongo.DomainModel.remove({space: {$in: spaceIds}}).exec()); + }; + + class DomainsService { + /** + * Batch merging domains. + * @param {Array.<mongo.DomainModel>} domains + */ + static batchMerge(domains) { + return _save(domains); + } + + /** + * Get domain and linked objects by space. + * @param {mongo.ObjectId|String} spaceIds - The space id that own domain. + * @returns {Promise.<[mongo.Cache[], mongo.Cluster[], mongo.DomainModel[], mongo.Space[]]>} + * contains requested domains and array of linked objects: caches, spaces. + */ + static listBySpaces(spaceIds) { + return mongo.DomainModel.find({space: {$in: spaceIds}}).sort('valueType').lean().exec(); + } + + /** + * Remove domain. + * @param {mongo.ObjectId|String} domainId - The domain id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(domainId) { + if (_.isNil(domainId)) + return Promise.reject(new errors.IllegalArgumentException('Domain id can not be undefined or null')); + + return mongo.Cache.update({domains: {$in: [domainId]}}, {$pull: {domains: domainId}}, {multi: true}).exec() + .then(() => mongo.DomainModel.remove({_id: domainId}).exec()) + .then(convertRemoveStatus); + } + + /** + * Remove all domains by user. + * @param {mongo.ObjectId|String} userId - The user id that own domain. + * @param {Boolean} demo - The flag indicates that need lookup in demo space. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static removeAll(userId, demo) { + return spacesService.spaceIds(userId, demo) + .then(removeAllBySpaces) + .then(convertRemoveStatus); + } + } + + return DomainsService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/igfss.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/igfss.js b/modules/web-console/backend/services/igfss.js new file mode 100644 index 0000000..20f0121 --- /dev/null +++ b/modules/web-console/backend/services/igfss.js @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/igfss', + inject: ['require(lodash)', 'mongo', 'services/spaces', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param {SpacesService} spacesService + * @param errors + * @returns {IgfssService} + */ +module.exports.factory = (_, mongo, spacesService, errors) => { + /** + * Convert remove status operation to own presentation. + * @param {RemoveResult} result - The results of remove operation. + */ + const convertRemoveStatus = ({result}) => ({rowsAffected: result.n}); + + /** + * Update existing IGFS + * @param {Object} igfs - The IGFS for updating + * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id + */ + const update = (igfs) => { + const igfsId = igfs._id; + + return mongo.Igfs.update({_id: igfsId}, igfs, {upsert: true}).exec() + .then(() => mongo.Cluster.update({_id: {$in: igfs.clusters}}, {$addToSet: {igfss: igfsId}}, {multi: true}).exec()) + .then(() => mongo.Cluster.update({_id: {$nin: igfs.clusters}}, {$pull: {igfss: igfsId}}, {multi: true}).exec()) + .then(() => igfs) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('IGFS with name: "' + igfs.name + '" already exist.'); + }); + }; + + /** + * Create new IGFS. + * @param {Object} igfs - The IGFS for creation. + * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id. + */ + const create = (igfs) => { + return mongo.Igfs.create(igfs) + .then((savedIgfs) => + mongo.Cluster.update({_id: {$in: savedIgfs.clusters}}, {$addToSet: {igfss: savedIgfs._id}}, {multi: true}).exec() + .then(() => savedIgfs) + ) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('IGFS with name: "' + igfs.name + '" already exist.'); + }); + }; + + /** + * Remove all IGFSs by space ids. + * @param {Number[]} spaceIds - The space ids for IGFS deletion. + * @returns {Promise.<RemoveResult>} - that resolves results of remove operation. + */ + const removeAllBySpaces = (spaceIds) => { + return mongo.Cluster.update({space: {$in: spaceIds}}, {igfss: []}, {multi: true}).exec() + .then(() => mongo.Igfs.remove({space: {$in: spaceIds}}).exec()); + }; + + class IgfssService { + /** + * Create or update IGFS. + * @param {Object} igfs - The IGFS + * @returns {Promise.<mongo.ObjectId>} that resolves IGFS id of merge operation. + */ + static merge(igfs) { + if (igfs._id) + return update(igfs); + + return create(igfs); + } + + /** + * Get IGFS by spaces. + * @param {mongo.ObjectId|String} spacesIds - The spaces ids that own IGFSs. + * @returns {Promise.<mongo.IGFS[]>} - contains requested IGFSs. + */ + static listBySpaces(spacesIds) { + return mongo.Igfs.find({space: {$in: spacesIds}}).sort('name').lean().exec(); + } + + /** + * Remove IGFS. + * @param {mongo.ObjectId|String} igfsId - The IGFS id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(igfsId) { + if (_.isNil(igfsId)) + return Promise.reject(new errors.IllegalArgumentException('IGFS id can not be undefined or null')); + + return mongo.Cluster.update({igfss: {$in: [igfsId]}}, {$pull: {igfss: igfsId}}, {multi: true}).exec() + .then(() => mongo.Igfs.remove({_id: igfsId}).exec()) + .then(convertRemoveStatus); + } + + /** + * Remove all IGFSes by user. + * @param {mongo.ObjectId|String} userId - The user id that own IGFS. + * @param {Boolean} demo - The flag indicates that need lookup in demo space. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static removeAll(userId, demo) { + return spacesService.spaceIds(userId, demo) + .then(removeAllBySpaces) + .then(convertRemoveStatus); + } + } + + return IgfssService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/mails.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/mails.js b/modules/web-console/backend/services/mails.js new file mode 100644 index 0000000..0700985 --- /dev/null +++ b/modules/web-console/backend/services/mails.js @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/mails', + inject: ['require(lodash)', 'require(nodemailer)', 'settings'] +}; + +/** + * @param _ + * @param nodemailer + * @param settings + * @returns {MailsService} + */ +module.exports.factory = (_, nodemailer, settings) => { + /** + * Send mail to user. + * + * @param {Account} user + * @param {String} subject + * @param {String} html + * @param {String} sendErr + * @throws {Error} + * @return {Promise} + */ + const send = (user, subject, html, sendErr) => { + return new Promise((resolve, reject) => { + const transportConfig = settings.smtp; + + if (_.isEmpty(transportConfig.service) || _.isEmpty(transportConfig.auth.user) || _.isEmpty(transportConfig.auth.pass)) + throw new Error('Failed to send email. SMTP server is not configured. Please ask webmaster to setup SMTP server!'); + + const mailer = nodemailer.createTransport(transportConfig); + + const sign = settings.smtp.sign ? `<br><br>--------------<br>${settings.smtp.sign}<br>` : ''; + + const mail = { + from: settings.smtp.from, + to: settings.smtp.address(`${user.firstName} ${user.lastName}`, user.email), + subject, + html: html + sign + }; + + mailer.sendMail(mail, (err) => { + if (err) + return reject(sendErr ? new Error(sendErr) : err); + + resolve(user); + }); + }); + }; + + class MailsService { + /** + * Send email to user for password reset. + * @param host + * @param user + */ + static emailUserSignUp(host, user) { + const resetLink = `${host}/password/reset?token=${user.resetPasswordToken}`; + + return send(user, `Thanks for signing up for ${settings.smtp.greeting}.`, + `Hello ${user.firstName} ${user.lastName}!<br><br>` + + `You are receiving this email because you have signed up to use <a href="${host}">${settings.smtp.greeting}</a>.<br><br>` + + 'If you have not done the sign up and do not know what this email is about, please ignore it.<br>' + + 'You may reset the password by clicking on the following link, or paste this into your browser:<br><br>' + + `<a href="${resetLink}">${resetLink}</a>`); + } + + /** + * Send email to user for password reset. + * @param host + * @param user + */ + static emailUserResetLink(host, user) { + const resetLink = `${host}/password/reset?token=${user.resetPasswordToken}`; + + return send(user, 'Password Reset', + `Hello ${user.firstName} ${user.lastName}!<br><br>` + + 'You are receiving this because you (or someone else) have requested the reset of the password for your account.<br><br>' + + 'Please click on the following link, or paste this into your browser to complete the process:<br><br>' + + `<a href="${resetLink}">${resetLink}</a><br><br>` + + 'If you did not request this, please ignore this email and your password will remain unchanged.', + 'Failed to send email with reset link!'); + } + + /** + * Send email to user for password reset. + * @param host + * @param user + */ + static emailPasswordChanged(host, user) { + return send(user, 'Your password has been changed', + `Hello ${user.firstName} ${user.lastName}!<br><br>` + + `This is a confirmation that the password for your account on <a href="${host}">${settings.smtp.greeting}</a> has just been changed.<br><br>`, + 'Password was changed, but failed to send confirmation email!'); + } + + /** + * Send email to user when it was deleted. + * @param host + * @param user + */ + static emailUserDeletion(host, user) { + return send(user, 'Your account was removed', + `Hello ${user.firstName} ${user.lastName}!<br><br>` + + `You are receiving this email because your account for <a href="${host}">${settings.smtp.greeting}</a> was removed.`, + 'Account was removed, but failed to send email notification to user!'); + } + } + + return MailsService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/notebooks.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/notebooks.js b/modules/web-console/backend/services/notebooks.js new file mode 100644 index 0000000..8846d8e --- /dev/null +++ b/modules/web-console/backend/services/notebooks.js @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/notebooks', + inject: ['require(lodash)', 'mongo', 'services/spaces', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param {SpacesService} spacesService + * @param errors + * @returns {NotebooksService} + */ +module.exports.factory = (_, mongo, spacesService, errors) => { + /** + * Convert remove status operation to own presentation. + * @param {RemoveResult} result - The results of remove operation. + */ + const convertRemoveStatus = ({result}) => ({rowsAffected: result.n}); + + /** + * Update existing notebook + * @param {Object} notebook - The notebook for updating + * @returns {Promise.<mongo.ObjectId>} that resolves cache id + */ + const update = (notebook) => { + return mongo.Notebook.findOneAndUpdate({_id: notebook._id}, notebook, {new: true, upsert: true}).exec() + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_UPDATE_ERROR || err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Notebook with name: "' + notebook.name + '" already exist.'); + }); + }; + + /** + * Create new notebook. + * @param {Object} notebook - The notebook for creation. + * @returns {Promise.<mongo.ObjectId>} that resolves cache id. + */ + const create = (notebook) => { + return mongo.Notebook.create(notebook) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException('Notebook with name: "' + notebook.name + '" already exist.'); + }); + }; + + class NotebooksService { + /** + * Create or update Notebook. + * @param {Object} notebook - The Notebook + * @returns {Promise.<mongo.ObjectId>} that resolves Notebook id of merge operation. + */ + static merge(notebook) { + if (notebook._id) + return update(notebook); + + return create(notebook); + } + + /** + * Get caches by spaces. + * @param {mongo.ObjectId|String} spaceIds - The spaces ids that own caches. + * @returns {Promise.<mongo.Cache[]>} - contains requested caches. + */ + static listBySpaces(spaceIds) { + return mongo.Notebook.find({space: {$in: spaceIds}}).sort('name').lean().exec(); + } + + /** + * Remove Notebook. + * @param {mongo.ObjectId|String} notebookId - The Notebook id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(notebookId) { + if (_.isNil(notebookId)) + return Promise.reject(new errors.IllegalArgumentException('Notebook id can not be undefined or null')); + + return mongo.Notebook.remove({_id: notebookId}).exec() + .then(convertRemoveStatus); + } + } + + return NotebooksService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/sessions.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/sessions.js b/modules/web-console/backend/services/sessions.js new file mode 100644 index 0000000..4fa95a3 --- /dev/null +++ b/modules/web-console/backend/services/sessions.js @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/sessions', + inject: ['require(lodash)', 'mongo', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param errors + * @returns {SessionsService} + */ +module.exports.factory = (_, mongo, errors) => { + class SessionsService { + /** + * Become user. + * @param {Session} session - current session of user. + * @param {mongo.ObjectId|String} viewedUserId - id of user to become. + */ + static become(session, viewedUserId) { + return mongo.Account.findById(viewedUserId).exec() + .then((viewedUser) => { + if (!session.req.user.admin) + throw new errors.IllegalAccessError('Became this user is not permitted. Only administrators can perform this actions.'); + + session.viewedUser = viewedUser; + }); + } + + /** + * Revert to your identity. + */ + static revert(session) { + return new Promise((resolve) => { + delete session.viewedUser; + + resolve(); + }); + } + } + + return SessionsService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/spaces.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/spaces.js b/modules/web-console/backend/services/spaces.js new file mode 100644 index 0000000..863d57c --- /dev/null +++ b/modules/web-console/backend/services/spaces.js @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/spaces', + inject: ['mongo', 'errors'] +}; + +/** + * @param mongo + * @param errors + * @returns {SpacesService} + */ +module.exports.factory = (mongo, errors) => { + class SpacesService { + /** + * Query for user spaces. + * + * @param {mongo.ObjectId|String} userId User ID. + * @param {Boolean} demo Is need use demo space. + * @returns {Promise} + */ + static spaces(userId, demo) { + return mongo.Space.find({owner: userId, demo: !!demo}).lean().exec() + .then((spaces) => { + if (!spaces.length) + throw new errors.MissingResourceException('Failed to find space'); + + return spaces; + }); + } + + /** + * Extract IDs from user spaces. + * + * @param {mongo.ObjectId|String} userId User ID. + * @param {Boolean} demo Is need use demo space. + * @returns {Promise} + */ + static spaceIds(userId, demo) { + return this.spaces(userId, demo) + .then((spaces) => spaces.map((space) => space._id)); + } + + /** + * Create demo space for user + * @param userId - user id + * @returns {Promise<mongo.Space>} that resolves created demo space for user + */ + static createDemoSpace(userId) { + return new mongo.Space({name: 'Demo space', owner: userId, demo: true}).save(); + } + } + + return SpacesService; +}; + http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/services/users.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/users.js b/modules/web-console/backend/services/users.js new file mode 100644 index 0000000..8058b25 --- /dev/null +++ b/modules/web-console/backend/services/users.js @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'services/users', + inject: ['require(lodash)', 'mongo', 'settings', 'services/spaces', 'services/mails', 'agent-manager', 'errors'] +}; + +/** + * @param _ + * @param mongo + * @param settings + * @param {SpacesService} spacesService + * @param {MailsService} mailsService + * @param agentMgr + * @param errors + * @returns {UsersService} + */ +module.exports.factory = (_, mongo, settings, spacesService, mailsService, agentMgr, errors) => { + const _randomString = () => { + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const possibleLen = possible.length; + + let res = ''; + + for (let i = 0; i < settings.tokenLength; i++) + res += possible.charAt(Math.floor(Math.random() * possibleLen)); + + return res; + }; + + class UsersService { + /** + * Save profile information. + * @param {String} host - The host + * @param {Object} user - The user + * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation. + */ + static create(host, user) { + return mongo.Account.count().exec() + .then((cnt) => { + user.admin = cnt === 0; + + user.token = _randomString(); + + return new mongo.Account(user); + }) + .then((created) => { + return new Promise((resolve, reject) => { + mongo.Account.register(created, user.password, (err, registered) => { + if (err) + reject(err); + + if (!registered) + reject(new errors.ServerErrorException('Failed to register user.')); + + resolve(registered); + }); + }); + }) + .then((registered) => { + registered.resetPasswordToken = _randomString(); + + return registered.save() + .then(() => mongo.Space.create({name: 'Personal space', owner: registered._id})) + .then(() => { + mailsService.emailUserSignUp(host, registered) + .catch((err) => console.error(err)); + + return registered; + }); + }); + } + + /** + * Save user. + * @param {Object} changed - The user + * @returns {Promise.<mongo.ObjectId>} that resolves account id of merge operation. + */ + static save(changed) { + return mongo.Account.findById(changed._id).exec() + .then((user) => { + if (!changed.password) + return Promise.resolve(user); + + return new Promise((resolve, reject) => { + user.setPassword(changed.password, (err, _user) => { + if (err) + return reject(err); + + delete changed.password; + + resolve(_user); + }); + }); + }) + .then((user) => { + if (!changed.email || user.email === changed.email) + return Promise.resolve(user); + + return new Promise((resolve, reject) => { + mongo.Account.findOne({email: changed.email}, (err, _user) => { + // TODO send error to admin + if (err) + reject(new Error('Failed to check email!')); + + if (_user && _user._id !== user._id) + reject(new Error('User with this email already registered!')); + + resolve(user); + }); + }); + }) + .then((user) => { + if (changed.token && user.token !== changed.token) + agentMgr.close(user._id, user.token); + + _.extend(user, changed); + + return user.save(); + }); + } + + /** + * Get list of user accounts and summary information. + * @returns {mongo.Account[]} - returns all accounts with counters object + */ + static list() { + return Promise.all([ + mongo.Space.aggregate([ + {$match: {demo: false}}, + {$lookup: {from: 'clusters', localField: '_id', foreignField: 'space', as: 'clusters'}}, + {$lookup: {from: 'caches', localField: '_id', foreignField: 'space', as: 'caches'}}, + {$lookup: {from: 'domainmodels', localField: '_id', foreignField: 'space', as: 'domainmodels'}}, + {$lookup: {from: 'igfs', localField: '_id', foreignField: 'space', as: 'igfs'}}, + { + $project: { + owner: 1, + clusters: {$size: '$clusters'}, + models: {$size: '$domainmodels'}, + caches: {$size: '$caches'}, + igfs: {$size: '$igfs'} + } + } + ]).exec(), + mongo.Account.find({}).sort('firstName lastName').lean().exec() + ]) + .then(([counters, users]) => { + const countersMap = _.keyBy(counters, 'owner'); + + _.forEach(users, (user) => { + user.counters = _.omit(countersMap[user._id], '_id', 'owner'); + }); + + return users; + }); + } + + /** + * Remove account. + * @param {String} host. + * @param {mongo.ObjectId|String} userId - The account id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(host, userId) { + return mongo.Account.findByIdAndRemove(userId).exec() + .then((user) => { + return spacesService.spaceIds(userId) + .then((spaceIds) => Promise.all([ + mongo.Cluster.remove({space: {$in: spaceIds}}).exec(), + mongo.Cache.remove({space: {$in: spaceIds}}).exec(), + mongo.DomainModel.remove({space: {$in: spaceIds}}).exec(), + mongo.Igfs.remove({space: {$in: spaceIds}}).exec(), + mongo.Notebook.remove({space: {$in: spaceIds}}).exec(), + mongo.Space.remove({owner: userId}).exec() + ])) + .catch((err) => console.error(`Failed to cleanup spaces [user=${user.username}, err=${err}`)) + .then(() => user); + }) + .then((user) => mailsService.emailUserDeletion(host, user).catch((err) => console.error(err))); + } + + /** + * Get account information. + */ + static get(user, viewedUser) { + if (_.isNil(user)) + return Promise.reject(new errors.AuthFailedException('The user profile service failed the sign in. User profile cannot be loaded.')); + + const becomeUsed = viewedUser && user.admin; + + if (becomeUsed) { + user = viewedUser; + + user.becomeUsed = true; + } + else + user = user.toJSON(); + + return mongo.Space.findOne({owner: user._id, demo: true}).exec() + .then((demoSpace) => { + if (user && demoSpace) + user.demoCreated = true; + + return user; + }); + } + } + + return UsersService; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/config/settings.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/config/settings.json b/modules/web-console/backend/test/config/settings.json new file mode 100644 index 0000000..a17a777 --- /dev/null +++ b/modules/web-console/backend/test/config/settings.json @@ -0,0 +1,20 @@ +{ + "server": { + "port": 3000, + "ssl": false + }, + "mongodb": { + "url": "mongodb://localhost/console-test" + }, + "agentServer": { + "port": 3001, + "ssl": false + }, + "mail": { + "service": "", + "sign": "Kind regards,<br>Apache Ignite Team", + "from": "Apache Ignite Web Console <[email protected]>", + "user": "[email protected]", + "pass": "" + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/data/accounts.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/data/accounts.json b/modules/web-console/backend/test/data/accounts.json new file mode 100644 index 0000000..e5b7f98 --- /dev/null +++ b/modules/web-console/backend/test/data/accounts.json @@ -0,0 +1,18 @@ +[ + { + "_id" : "57725443e6d604c05dab9ded", + "salt" : "ca8b49c2eacd498a0973de30c0873c166ed99fa0605981726aedcc85bee17832", + "hash" : "c052c87e454cd0875332719e1ce085ccd92bedb73c8f939ba45d387f724da97128280643ad4f841d929d48de802f48f4a27b909d2dc806d957d38a1a4049468ce817490038f00ac1416aaf9f8f5a5c476730b46ea22d678421cd269869d4ba9d194f73906e5d5a4fec5229459e20ebda997fb95298067126f6c15346d886d44b67def03bf3ffe484b2e4fa449985de33a0c12e4e1da4c7d71fe7af5d138433f703d8c7eeebbb3d57f1a89659010a1f1d3cd4fbc524abab07860daabb08f08a28b8bfc64ecde2ea3c103030d0d54fc24d9c02f92ee6b3aa1bcd5c70113ab9a8045faea7dd2dc59ec4f9f69fcf634232721e9fb44012f0e8c8fdf7c6bf642db6867ef8e7877123e1bc78af7604fee2e34ad0191f8b97613ea458e0fca024226b7055e08a4bdb256fabf0a203a1e5b6a6c298fb0c60308569cefba779ce1e41fb971e5d1745959caf524ab0bedafce67157922f9c505cea033f6ed28204791470d9d08d31ce7e8003df8a3a05282d4d60bfe6e2f7de06f4b18377dac0fe764ed683c9b2553e75f8280c748aa166fef6f89190b1c6d369ab86422032171e6f9686de42ac65708e63bf018a043601d85bc5c820c7ad1d51ded32e59cdaa629a3f7ae325bbc931f9f21d90c9204effdbd53721a60c8b180dd8c236133e287a47ccc9e5072eb6593771e435e4d5196 d50d6ddb32c226651c6503387895c5ad025f69fd3", + "email" : "a@a", + "firstName" : "TestFirstName", + "lastName" : "TestLastName", + "company" : "TestCompany", + "country" : "Canada", + "admin" : true, + "token" : "ppw4tPI3JUOGHva8CODO", + "attempts" : 0, + "lastLogin" : "2016-06-28T10:41:07.463Z", + "__v" : 0, + "resetPasswordToken" : "892rnLbEnVp1FP75Jgpi" + } +] \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/data/caches.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/data/caches.json b/modules/web-console/backend/test/data/caches.json new file mode 100644 index 0000000..f7a8690 --- /dev/null +++ b/modules/web-console/backend/test/data/caches.json @@ -0,0 +1,87 @@ +[ + { + "name": "CarCache", + "cacheMode": "PARTITIONED", + "atomicityMode": "ATOMIC", + "readThrough": true, + "writeThrough": true, + "sqlFunctionClasses": [], + "cacheStoreFactory": { + "kind": "CacheJdbcPojoStoreFactory", + "CacheJdbcPojoStoreFactory": { + "dataSourceBean": "dsH2", + "dialect": "H2" + } + }, + "domains": [], + "clusters": [] + }, + { + "name": "ParkingCache", + "cacheMode": "PARTITIONED", + "atomicityMode": "ATOMIC", + "readThrough": true, + "writeThrough": true, + "sqlFunctionClasses": [], + "cacheStoreFactory": { + "kind": "CacheJdbcPojoStoreFactory", + "CacheJdbcPojoStoreFactory": { + "dataSourceBean": "dsH2", + "dialect": "H2" + } + }, + "domains": [], + "clusters": [] + }, + { + "name": "CountryCache", + "cacheMode": "PARTITIONED", + "atomicityMode": "ATOMIC", + "readThrough": true, + "writeThrough": true, + "sqlFunctionClasses": [], + "cacheStoreFactory": { + "kind": "CacheJdbcPojoStoreFactory", + "CacheJdbcPojoStoreFactory": { + "dataSourceBean": "dsH2", + "dialect": "H2" + } + }, + "domains": [], + "clusters": [] + }, + { + "name": "DepartmentCache", + "cacheMode": "PARTITIONED", + "atomicityMode": "ATOMIC", + "readThrough": true, + "writeThrough": true, + "sqlFunctionClasses": [], + "cacheStoreFactory": { + "kind": "CacheJdbcPojoStoreFactory", + "CacheJdbcPojoStoreFactory": { + "dataSourceBean": "dsH2", + "dialect": "H2" + } + }, + "domains": [], + "clusters": [] + }, + { + "name": "EmployeeCache", + "cacheMode": "PARTITIONED", + "atomicityMode": "ATOMIC", + "readThrough": true, + "writeThrough": true, + "sqlFunctionClasses": [], + "cacheStoreFactory": { + "kind": "CacheJdbcPojoStoreFactory", + "CacheJdbcPojoStoreFactory": { + "dataSourceBean": "dsH2", + "dialect": "H2" + } + }, + "domains": [], + "clusters": [] + } +] http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/data/clusters.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/data/clusters.json b/modules/web-console/backend/test/data/clusters.json new file mode 100644 index 0000000..014b519 --- /dev/null +++ b/modules/web-console/backend/test/data/clusters.json @@ -0,0 +1,50 @@ +[ + { + "name": "cluster-igfs", + "connector": { + "noDelay": true + }, + "communication": { + "tcpNoDelay": true + }, + "igfss": [], + "caches": [], + "binaryConfiguration": { + "compactFooter": true, + "typeConfigurations": [] + }, + "discovery": { + "kind": "Multicast", + "Multicast": { + "addresses": ["127.0.0.1:47500..47510"] + }, + "Vm": { + "addresses": ["127.0.0.1:47500..47510"] + } + } + }, + { + "name": "cluster-caches", + "connector": { + "noDelay": true + }, + "communication": { + "tcpNoDelay": true + }, + "igfss": [], + "caches": [], + "binaryConfiguration": { + "compactFooter": true, + "typeConfigurations": [] + }, + "discovery": { + "kind": "Multicast", + "Multicast": { + "addresses": ["127.0.0.1:47500..47510"] + }, + "Vm": { + "addresses": ["127.0.0.1:47500..47510"] + } + } + } +] http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/data/domains.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/data/domains.json b/modules/web-console/backend/test/data/domains.json new file mode 100644 index 0000000..980d8d1 --- /dev/null +++ b/modules/web-console/backend/test/data/domains.json @@ -0,0 +1,307 @@ +[ + { + "keyType": "Integer", + "valueType": "model.Parking", + "queryMetadata": "Configuration", + "databaseSchema": "CARS", + "databaseTable": "PARKING", + "indexes": [], + "aliases": [], + "fields": [ + { + "name": "name", + "className": "String" + }, + { + "name": "capacity", + "className": "Integer" + } + ], + "valueFields": [ + { + "databaseFieldName": "NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "name", + "javaFieldType": "String" + }, + { + "databaseFieldName": "CAPACITY", + "databaseFieldType": "INTEGER", + "javaFieldName": "capacity", + "javaFieldType": "int" + } + ], + "keyFields": [ + { + "databaseFieldName": "ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "id", + "javaFieldType": "int" + } + ], + "caches": [] + }, + { + "keyType": "Integer", + "valueType": "model.Department", + "queryMetadata": "Configuration", + "databaseSchema": "PUBLIC", + "databaseTable": "DEPARTMENT", + "indexes": [], + "aliases": [], + "fields": [ + { + "name": "countryId", + "className": "Integer" + }, + { + "name": "name", + "className": "String" + } + ], + "valueFields": [ + { + "databaseFieldName": "COUNTRY_ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "countryId", + "javaFieldType": "int" + }, + { + "databaseFieldName": "NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "name", + "javaFieldType": "String" + } + ], + "keyFields": [ + { + "databaseFieldName": "ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "id", + "javaFieldType": "int" + } + ], + "caches": [] + }, + { + "keyType": "Integer", + "valueType": "model.Employee", + "queryMetadata": "Configuration", + "databaseSchema": "PUBLIC", + "databaseTable": "EMPLOYEE", + "indexes": [ + { + "name": "EMP_NAMES", + "indexType": "SORTED", + "fields": [ + { + "name": "firstName", + "direction": true + }, + { + "name": "lastName", + "direction": true + } + ] + }, + { + "name": "EMP_SALARY", + "indexType": "SORTED", + "fields": [ + { + "name": "salary", + "direction": true + } + ] + } + ], + "aliases": [], + "fields": [ + { + "name": "departmentId", + "className": "Integer" + }, + { + "name": "managerId", + "className": "Integer" + }, + { + "name": "firstName", + "className": "String" + }, + { + "name": "lastName", + "className": "String" + }, + { + "name": "email", + "className": "String" + }, + { + "name": "phoneNumber", + "className": "String" + }, + { + "name": "hireDate", + "className": "Date" + }, + { + "name": "job", + "className": "String" + }, + { + "name": "salary", + "className": "Double" + } + ], + "valueFields": [ + { + "databaseFieldName": "DEPARTMENT_ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "departmentId", + "javaFieldType": "int" + }, + { + "databaseFieldName": "MANAGER_ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "managerId", + "javaFieldType": "Integer" + }, + { + "databaseFieldName": "FIRST_NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "firstName", + "javaFieldType": "String" + }, + { + "databaseFieldName": "LAST_NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "lastName", + "javaFieldType": "String" + }, + { + "databaseFieldName": "EMAIL", + "databaseFieldType": "VARCHAR", + "javaFieldName": "email", + "javaFieldType": "String" + }, + { + "databaseFieldName": "PHONE_NUMBER", + "databaseFieldType": "VARCHAR", + "javaFieldName": "phoneNumber", + "javaFieldType": "String" + }, + { + "databaseFieldName": "HIRE_DATE", + "databaseFieldType": "DATE", + "javaFieldName": "hireDate", + "javaFieldType": "Date" + }, + { + "databaseFieldName": "JOB", + "databaseFieldType": "VARCHAR", + "javaFieldName": "job", + "javaFieldType": "String" + }, + { + "databaseFieldName": "SALARY", + "databaseFieldType": "DOUBLE", + "javaFieldName": "salary", + "javaFieldType": "Double" + } + ], + "keyFields": [ + { + "databaseFieldName": "ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "id", + "javaFieldType": "int" + } + ], + "caches": [] + }, + { + "keyType": "Integer", + "valueType": "model.Country", + "queryMetadata": "Configuration", + "databaseSchema": "PUBLIC", + "databaseTable": "COUNTRY", + "indexes": [], + "aliases": [], + "fields": [ + { + "name": "name", + "className": "String" + }, + { + "name": "population", + "className": "Integer" + } + ], + "valueFields": [ + { + "databaseFieldName": "NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "name", + "javaFieldType": "String" + }, + { + "databaseFieldName": "POPULATION", + "databaseFieldType": "INTEGER", + "javaFieldName": "population", + "javaFieldType": "int" + } + ], + "keyFields": [ + { + "databaseFieldName": "ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "id", + "javaFieldType": "int" + } + ], + "caches": [] + }, + { + "keyType": "Integer", + "valueType": "model.Car", + "queryMetadata": "Configuration", + "databaseSchema": "CARS", + "databaseTable": "CAR", + "indexes": [], + "aliases": [], + "fields": [ + { + "name": "parkingId", + "className": "Integer" + }, + { + "name": "name", + "className": "String" + } + ], + "valueFields": [ + { + "databaseFieldName": "PARKING_ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "parkingId", + "javaFieldType": "int" + }, + { + "databaseFieldName": "NAME", + "databaseFieldType": "VARCHAR", + "javaFieldName": "name", + "javaFieldType": "String" + } + ], + "keyFields": [ + { + "databaseFieldName": "ID", + "databaseFieldType": "INTEGER", + "javaFieldName": "id", + "javaFieldType": "int" + } + ], + "caches": [] + } +] http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/data/igfss.json ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/data/igfss.json b/modules/web-console/backend/test/data/igfss.json new file mode 100644 index 0000000..cd128a6 --- /dev/null +++ b/modules/web-console/backend/test/data/igfss.json @@ -0,0 +1,10 @@ +[ + { + "ipcEndpointEnabled": true, + "fragmentizerEnabled": true, + "name": "igfs", + "dataCacheName": "igfs-data", + "metaCacheName": "igfs-meta", + "clusters": [] + } +] http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/injector.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/injector.js b/modules/web-console/backend/test/injector.js new file mode 100644 index 0000000..8d44d31 --- /dev/null +++ b/modules/web-console/backend/test/injector.js @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import fireUp from 'fire-up'; + +module.exports = fireUp.newInjector({ + basePath: path.join(__dirname, '../'), + modules: [ + './app/**/*.js', + './config/**/*.js', + './errors/**/*.js', + './middlewares/**/*.js', + './routes/**/*.js', + './services/**/*.js' + ] +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/unit/CacheService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/CacheService.test.js b/modules/web-console/backend/test/unit/CacheService.test.js new file mode 100644 index 0000000..1442775 --- /dev/null +++ b/modules/web-console/backend/test/unit/CacheService.test.js @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import {assert} from 'chai'; +import injector from '../injector'; +import testCaches from '../data/caches.json'; +import testAccounts from '../data/accounts.json'; + +let cacheService; +let mongo; +let errors; + +suite('CacheServiceTestsSuite', () => { + const prepareUserSpaces = () => { + return mongo.Account.create(testAccounts) + .then((accounts) => { + return Promise.all( + accounts.map((account) => mongo.Space.create( + [ + {name: 'Personal space', owner: account._id, demo: false}, + {name: 'Demo space', owner: account._id, demo: true} + ] + ))) + .then((spaces) => [accounts, spaces]); + }); + }; + + suiteSetup(() => { + return Promise.all([injector('services/caches'), + injector('mongo'), + injector('errors')]) + .then(([_cacheService, _mongo, _errors]) => { + mongo = _mongo; + cacheService = _cacheService; + errors = _errors; + }); + }); + + setup(() => { + return Promise.all([ + mongo.Cache.remove().exec(), + mongo.Account.remove().exec(), + mongo.Space.remove().exec() + ]); + }); + + test('Create new cache', (done) => { + cacheService.merge(testCaches[0]) + .then((cache) => { + assert.isNotNull(cache._id); + + return cache._id; + }) + .then((cacheId) => mongo.Cache.findById(cacheId)) + .then((cache) => { + assert.isNotNull(cache); + }) + .then(done) + .catch(done); + }); + + test('Update existed cache', (done) => { + const newName = 'NewUniqueName'; + + cacheService.merge(testCaches[0]) + .then((cache) => { + const cacheBeforeMerge = {...testCaches[0], _id: cache._id, name: newName}; + + return cacheService.merge(cacheBeforeMerge); + }) + .then((cache) => mongo.Cache.findById(cache._id)) + .then((cacheAfterMerge) => { + assert.equal(cacheAfterMerge.name, newName); + }) + .then(done) + .catch(done); + }); + + test('Create duplicated cache', (done) => { + cacheService.merge(testCaches[0]) + .then(() => cacheService.merge(testCaches[0])) + .catch((err) => { + assert.instanceOf(err, errors.DuplicateKeyException); + + done(); + }); + }); + + test('Remove existed cache', (done) => { + cacheService.merge(testCaches[0]) + .then((createdCache) => { + return mongo.Cache.findById(createdCache._id) + .then((foundCache) => foundCache._id) + .then(cacheService.remove) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(() => mongo.Cache.findById(createdCache._id)) + .then((notFoundCache) => { + assert.isNull(notFoundCache); + }); + }) + .then(done) + .catch(done); + }); + + test('Remove cache without identifier', (done) => { + cacheService.merge(testCaches[0]) + .then(() => cacheService.remove()) + .catch((err) => { + assert.instanceOf(err, errors.IllegalArgumentException); + + done(); + }); + }); + + test('Remove missed cache', (done) => { + const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF'; + + cacheService.merge(testCaches[0]) + .then(() => cacheService.remove(validNoExistingId)) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 0); + }) + .then(done) + .catch(done); + }); + + test('Remove all caches in space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const currentUser = accounts[0]; + const userCache = {...testCaches[0], space: spaces[0][0]._id}; + + return cacheService.merge(userCache) + .then(() => cacheService.removeAll(currentUser._id, false)); + }) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(done) + .catch(done); + }); + + test('Get all caches by space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const userCache = {...testCaches[0], space: spaces[0][0]._id}; + + return cacheService.merge(userCache) + .then((cache) => { + return cacheService.listBySpaces(spaces[0][0]._id) + .then((caches) => { + assert.equal(caches.length, 1); + assert.equal(caches[0]._id.toString(), cache._id.toString()); + }); + }); + }) + .then(done) + .catch(done); + }); + + test('Update linked entities on update cache', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove cache', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove all caches in space', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/unit/ClusterService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/ClusterService.test.js b/modules/web-console/backend/test/unit/ClusterService.test.js new file mode 100644 index 0000000..ab0e912 --- /dev/null +++ b/modules/web-console/backend/test/unit/ClusterService.test.js @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import {assert} from 'chai'; +import injector from '../injector'; +import testClusters from '../data/clusters.json'; +import testAccounts from '../data/accounts.json'; + +let clusterService; +let mongo; +let errors; + +suite('ClusterServiceTestsSuite', () => { + const prepareUserSpaces = () => { + return mongo.Account.create(testAccounts) + .then((accounts) => { + return Promise.all(accounts.map((account) => mongo.Space.create( + [ + {name: 'Personal space', owner: account._id, demo: false}, + {name: 'Demo space', owner: account._id, demo: true} + ] + ))) + .then((spaces) => [accounts, spaces]); + }); + }; + + suiteSetup(() => { + return Promise.all([injector('services/clusters'), + injector('mongo'), + injector('errors')]) + .then(([_clusterService, _mongo, _errors]) => { + mongo = _mongo; + clusterService = _clusterService; + errors = _errors; + }); + }); + + setup(() => { + return Promise.all([ + mongo.Cluster.remove().exec(), + mongo.Account.remove().exec(), + mongo.Space.remove().exec() + ]); + }); + + test('Create new cluster', (done) => { + clusterService.merge(testClusters[0]) + .then((cluster) => { + assert.isNotNull(cluster._id); + + return cluster._id; + }) + .then((clusterId) => mongo.Cluster.findById(clusterId)) + .then((cluster) => { + assert.isNotNull(cluster); + }) + .then(done) + .catch(done); + }); + + test('Update existed cluster', (done) => { + const newName = 'NewUniqueName'; + + clusterService.merge(testClusters[0]) + .then((cluster) => { + const clusterBeforeMerge = {...testClusters[0], _id: cluster._id, name: newName}; + + return clusterService.merge(clusterBeforeMerge); + }) + .then((cluster) => mongo.Cluster.findById(cluster._id)) + .then((clusterAfterMerge) => { + assert.equal(clusterAfterMerge.name, newName); + }) + .then(done) + .catch(done); + }); + + test('Create duplicated cluster', (done) => { + clusterService.merge(testClusters[0]) + .then(() => clusterService.merge(testClusters[0])) + .catch((err) => { + assert.instanceOf(err, errors.DuplicateKeyException); + + done(); + }); + }); + + test('Remove existed cluster', (done) => { + clusterService.merge(testClusters[0]) + .then((existCluster) => { + return mongo.Cluster.findById(existCluster._id) + .then((foundCluster) => clusterService.remove(foundCluster._id)) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(() => mongo.Cluster.findById(existCluster._id)) + .then((notFoundCluster) => { + assert.isNull(notFoundCluster); + }); + }) + .then(done) + .catch(done); + }); + + test('Remove cluster without identifier', (done) => { + clusterService.merge(testClusters[0]) + .then(() => clusterService.remove()) + .catch((err) => { + assert.instanceOf(err, errors.IllegalArgumentException); + + done(); + }); + }); + + test('Remove missed cluster', (done) => { + const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF'; + + clusterService.merge(testClusters[0]) + .then(() => clusterService.remove(validNoExistingId)) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 0); + }) + .then(done) + .catch(done); + }); + + test('Remove all clusters in space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const currentUser = accounts[0]; + const userCluster = {...testClusters[0], space: spaces[0][0]._id}; + + return clusterService.merge(userCluster) + .then(() => clusterService.removeAll(currentUser._id, false)); + }) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(done) + .catch(done); + }); + + test('Get all clusters by space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const userCluster = {...testClusters[0], space: spaces[0][0]._id}; + + return clusterService.merge(userCluster) + .then((existCluster) => { + return clusterService.listBySpaces(spaces[0][0]._id) + .then((clusters) => { + assert.equal(clusters.length, 1); + assert.equal(clusters[0]._id.toString(), existCluster._id.toString()); + }); + }); + }) + .then(done) + .catch(done); + }); + + test('Update linked entities on update cluster', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove cluster', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove all clusters in space', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/test/unit/DomainService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/DomainService.test.js b/modules/web-console/backend/test/unit/DomainService.test.js new file mode 100644 index 0000000..477b454 --- /dev/null +++ b/modules/web-console/backend/test/unit/DomainService.test.js @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import {assert} from 'chai'; +import injector from '../injector'; +import testDomains from '../data/domains.json'; +import testAccounts from '../data/accounts.json'; + +let domainService; +let mongo; +let errors; + +suite('DomainsServiceTestsSuite', () => { + const prepareUserSpaces = () => { + return mongo.Account.create(testAccounts) + .then((accounts) => { + return Promise.all(accounts.map((account) => mongo.Space.create( + [ + {name: 'Personal space', owner: account._id, demo: false}, + {name: 'Demo space', owner: account._id, demo: true} + ] + ))) + .then((spaces) => [accounts, spaces]); + }); + }; + + suiteSetup(() => { + return Promise.all([injector('services/domains'), + injector('mongo'), + injector('errors')]) + .then(([_domainService, _mongo, _errors]) => { + mongo = _mongo; + domainService = _domainService; + errors = _errors; + }); + }); + + setup(() => { + return Promise.all([ + mongo.DomainModel.remove().exec(), + mongo.Account.remove().exec(), + mongo.Space.remove().exec() + ]); + }); + + test('Create new domain', (done) => { + domainService.batchMerge([testDomains[0]]) + .then((results) => { + const domain = results.savedDomains[0]; + + assert.isNotNull(domain._id); + + return domain._id; + }) + .then((domainId) => mongo.DomainModel.findById(domainId)) + .then((domain) => { + assert.isNotNull(domain); + }) + .then(done) + .catch(done); + }); + + test('Update existed domain', (done) => { + const newValType = 'value.Type'; + + domainService.batchMerge([testDomains[0]]) + .then((results) => { + const domain = results.savedDomains[0]; + + const domainBeforeMerge = {...testDomains[0], _id: domain._id, valueType: newValType}; + + return domainService.batchMerge([domainBeforeMerge]); + }) + .then((results) => mongo.DomainModel.findById(results.savedDomains[0]._id)) + .then((domainAfterMerge) => { + assert.equal(domainAfterMerge.valueType, newValType); + }) + .then(done) + .catch(done); + }); + + test('Create duplicated domain', (done) => { + domainService.batchMerge([testDomains[0]]) + .then(() => domainService.batchMerge([testDomains[0]])) + .catch((err) => { + assert.instanceOf(err, errors.DuplicateKeyException); + + done(); + }); + }); + + test('Remove existed domain', (done) => { + domainService.batchMerge([testDomains[0]]) + .then((results) => { + const domain = results.savedDomains[0]; + + return mongo.DomainModel.findById(domain._id) + .then((foundDomain) => domainService.remove(foundDomain._id)) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(() => mongo.DomainModel.findById(domain._id)) + .then((notFoundDomain) => { + assert.isNull(notFoundDomain); + }); + }) + .then(done) + .catch(done); + }); + + test('Remove domain without identifier', (done) => { + domainService.batchMerge([testDomains[0]]) + .then(() => domainService.remove()) + .catch((err) => { + assert.instanceOf(err, errors.IllegalArgumentException); + + done(); + }); + }); + + test('Remove missed domain', (done) => { + const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF'; + + domainService.batchMerge([testDomains[0]]) + .then(() => domainService.remove(validNoExistingId)) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 0); + }) + .then(done) + .catch(done); + }); + + test('Remove all domains in space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const currentUser = accounts[0]; + const userDomain = {...testDomains[0], space: spaces[0][0]._id}; + + return domainService.batchMerge([userDomain]) + .then(() => domainService.removeAll(currentUser._id, false)); + }) + .then(({rowsAffected}) => { + assert.equal(rowsAffected, 1); + }) + .then(done) + .catch(done); + }); + + test('Get all domains by space', (done) => { + prepareUserSpaces() + .then(([accounts, spaces]) => { + const userDomain = {...testDomains[0], space: spaces[0][0]._id}; + + return domainService.batchMerge([userDomain]) + .then((results) => { + const domain = results.savedDomains[0]; + + return domainService.listBySpaces(spaces[0][0]._id) + .then((domains) => { + assert.equal(domains.length, 1); + assert.equal(domains[0]._id.toString(), domain._id.toString()); + }); + }); + }) + .then(done) + .catch(done); + }); + + test('Update linked entities on update domain', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove domain', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); + + test('Update linked entities on remove all domains in space', (done) => { + // TODO IGNITE-3262 Add test. + done(); + }); +});
