mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Add technical support for projects
This commit is contained in:
		
							parent
							
								
									144e832cdc
								
							
						
					
					
						commit
						b644071a34
					
				| @ -36,6 +36,7 @@ module.exports = function(config) { | ||||
|     app.use(responseTime(config)); | ||||
|     app.use(requestLogger(config)); | ||||
|     app.use(secureHeaders(config)); | ||||
|     app.use(express.urlencoded({ extended: true })); | ||||
| 
 | ||||
|     if (config.publicFolder) { | ||||
|         app.use(favicon(path.join(config.publicFolder, 'favicon.ico'))); | ||||
|  | ||||
| @ -16,6 +16,7 @@ const FEATURE_COLUMNS = [ | ||||
|     'name', | ||||
|     'description', | ||||
|     'type', | ||||
|     'project', | ||||
|     'enabled', | ||||
|     'stale', | ||||
|     'strategies', | ||||
| @ -65,6 +66,14 @@ class FeatureToggleStore { | ||||
|         return rows.map(this.rowToFeature); | ||||
|     } | ||||
| 
 | ||||
|     async getFeaturesBy(fields) { | ||||
|         const rows = await this.db | ||||
|             .select(FEATURE_COLUMNS) | ||||
|             .from(TABLE) | ||||
|             .where(fields); | ||||
|         return rows.map(this.rowToFeature); | ||||
|     } | ||||
| 
 | ||||
|     async count() { | ||||
|         return this.db | ||||
|             .count('*') | ||||
| @ -114,6 +123,7 @@ class FeatureToggleStore { | ||||
|             name: row.name, | ||||
|             description: row.description, | ||||
|             type: row.type, | ||||
|             project: row.project, | ||||
|             enabled: row.enabled > 0, | ||||
|             stale: row.stale, | ||||
|             strategies: row.strategies, | ||||
| @ -127,6 +137,7 @@ class FeatureToggleStore { | ||||
|             name: data.name, | ||||
|             description: data.description, | ||||
|             type: data.type, | ||||
|             project: data.project, | ||||
|             enabled: data.enabled ? 1 : 0, | ||||
|             stale: data.stale, | ||||
|             archived: data.archived ? 1 : 0, | ||||
|  | ||||
| @ -12,6 +12,7 @@ const ClientApplicationsStore = require('./client-applications-store'); | ||||
| const ContextFieldStore = require('./context-field-store'); | ||||
| const SettingStore = require('./setting-store'); | ||||
| const UserStore = require('./user-store'); | ||||
| const ProjectStore = require('./project-store'); | ||||
| 
 | ||||
| module.exports.createStores = (config, eventBus) => { | ||||
|     const { getLogger } = config; | ||||
| @ -49,5 +50,6 @@ module.exports.createStores = (config, eventBus) => { | ||||
|         ), | ||||
|         settingStore: new SettingStore(db, getLogger), | ||||
|         userStore: new UserStore(db, getLogger), | ||||
|         projectStore: new ProjectStore(db, getLogger), | ||||
|     }; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										95
									
								
								lib/db/project-store.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								lib/db/project-store.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| const NotFoundError = require('../error/notfound-error'); | ||||
| 
 | ||||
| const COLUMNS = ['id', 'name', 'description', 'created_at']; | ||||
| const TABLE = 'projects'; | ||||
| 
 | ||||
| class ProjectStore { | ||||
|     constructor(db, getLogger) { | ||||
|         this.db = db; | ||||
|         this.logger = getLogger('project-store.js'); | ||||
|     } | ||||
| 
 | ||||
|     fieldToRow(data) { | ||||
|         return { | ||||
|             id: data.id, | ||||
|             name: data.name, | ||||
|             description: data.description, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async getAll() { | ||||
|         const rows = await this.db | ||||
|             .select(COLUMNS) | ||||
|             .from(TABLE) | ||||
|             .orderBy('name', 'asc'); | ||||
| 
 | ||||
|         return rows.map(this.mapRow); | ||||
|     } | ||||
| 
 | ||||
|     async get(id) { | ||||
|         return this.db | ||||
|             .first(COLUMNS) | ||||
|             .from(TABLE) | ||||
|             .where({ id }) | ||||
|             .then(this.mapRow); | ||||
|     } | ||||
| 
 | ||||
|     async hasProject(id) { | ||||
|         return this.db | ||||
|             .first('id') | ||||
|             .from(TABLE) | ||||
|             .where({ id }) | ||||
|             .then(row => { | ||||
|                 if (!row) { | ||||
|                     throw new NotFoundError(`No project with id=${id} found`); | ||||
|                 } | ||||
|                 return { | ||||
|                     id: row.id, | ||||
|                     archived: row.archived === 1, | ||||
|                 }; | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     async create(project) { | ||||
|         await this.db(TABLE) | ||||
|             .insert(this.fieldToRow(project)) | ||||
|             .catch(err => | ||||
|                 this.logger.error('Could not insert project, error: ', err), | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     async update(data) { | ||||
|         try { | ||||
|             await this.db(TABLE) | ||||
|                 .where({ id: data.id }) | ||||
|                 .update(this.fieldToRow(data)); | ||||
|         } catch (err) { | ||||
|             this.logger.error('Could not update project, error: ', err); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async delete(id) { | ||||
|         try { | ||||
|             await this.db(TABLE) | ||||
|                 .where({ id }) | ||||
|                 .del(); | ||||
|         } catch (err) { | ||||
|             this.logger.error('Could not delete project, error: ', err); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     mapRow(row) { | ||||
|         if (!row) { | ||||
|             throw new NotFoundError('No project found'); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             id: row.id, | ||||
|             name: row.name, | ||||
|             description: row.description, | ||||
|             createdAt: row.created_at, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = ProjectStore; | ||||
							
								
								
									
										26
									
								
								lib/error/invalid-operation-error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/error/invalid-operation-error.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| class InvalidOperationError extends Error { | ||||
|     constructor(message) { | ||||
|         super(); | ||||
|         Error.captureStackTrace(this, this.constructor); | ||||
| 
 | ||||
|         this.name = this.constructor.name; | ||||
|         this.message = message; | ||||
|     } | ||||
| 
 | ||||
|     toJSON() { | ||||
|         const obj = { | ||||
|             isJoi: true, | ||||
|             name: this.constructor.name, | ||||
|             details: [ | ||||
|                 { | ||||
|                     message: this.message, | ||||
|                 }, | ||||
|             ], | ||||
|         }; | ||||
|         return obj; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = InvalidOperationError; | ||||
| @ -18,6 +18,9 @@ const { | ||||
|     CONTEXT_FIELD_CREATED, | ||||
|     CONTEXT_FIELD_UPDATED, | ||||
|     CONTEXT_FIELD_DELETED, | ||||
|     PROJECT_CREATED, | ||||
|     PROJECT_UPDATED, | ||||
|     PROJECT_DELETED, | ||||
| } = require('./event-type'); | ||||
| 
 | ||||
| const strategyTypes = [ | ||||
| @ -43,6 +46,8 @@ const contextTypes = [ | ||||
|     CONTEXT_FIELD_UPDATED, | ||||
| ]; | ||||
| 
 | ||||
| const projectTypes = [PROJECT_CREATED, PROJECT_UPDATED, PROJECT_DELETED]; | ||||
| 
 | ||||
| function baseTypeFor(event) { | ||||
|     if (featureTypes.indexOf(event.type) !== -1) { | ||||
|         return 'features'; | ||||
| @ -53,6 +58,9 @@ function baseTypeFor(event) { | ||||
|     if (contextTypes.indexOf(event.type) !== -1) { | ||||
|         return 'context'; | ||||
|     } | ||||
|     if (projectTypes.indexOf(event.type) !== -1) { | ||||
|         return 'project'; | ||||
|     } | ||||
|     throw new Error(`unknown event type: ${JSON.stringify(event)}`); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -15,4 +15,7 @@ module.exports = { | ||||
|     CONTEXT_FIELD_CREATED: 'context-field-created', | ||||
|     CONTEXT_FIELD_UPDATED: 'context-field-updated', | ||||
|     CONTEXT_FIELD_DELETED: 'context-field-deleted', | ||||
|     PROJECT_CREATED: 'project-created', | ||||
|     PROJECT_UPDATED: 'project-updated', | ||||
|     PROJECT_DELETED: 'project-deleted', | ||||
| }; | ||||
|  | ||||
| @ -12,6 +12,9 @@ const UPDATE_APPLICATION = 'UPDATE_APPLICATION'; | ||||
| const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD'; | ||||
| const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD'; | ||||
| const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; | ||||
| const CREATE_PROJECT = 'CREATE_PROJECT'; | ||||
| const UPDATE_PROJECT = 'UPDATE_PROJECT'; | ||||
| const DELETE_PROJECT = 'DELETE_PROJECT'; | ||||
| 
 | ||||
| module.exports = { | ||||
|     ADMIN, | ||||
| @ -26,4 +29,7 @@ module.exports = { | ||||
|     CREATE_CONTEXT_FIELD, | ||||
|     UPDATE_CONTEXT_FIELD, | ||||
|     DELETE_CONTEXT_FIELD, | ||||
|     CREATE_PROJECT, | ||||
|     UPDATE_PROJECT, | ||||
|     DELETE_PROJECT, | ||||
| }; | ||||
|  | ||||
| @ -65,6 +65,7 @@ const featureShema = joi | ||||
|         enabled: joi.boolean().default(false), | ||||
|         stale: joi.boolean().default(false), | ||||
|         type: joi.string().default('release'), | ||||
|         project: joi.string().default('default'), | ||||
|         description: joi | ||||
|             .string() | ||||
|             .allow('') | ||||
|  | ||||
| @ -17,14 +17,12 @@ test('should require URL firendly name', t => { | ||||
| test('should be valid toggle name', t => { | ||||
|     const toggle = { | ||||
|         name: 'app.name', | ||||
|         type: 'release', | ||||
|         enabled: false, | ||||
|         stale: false, | ||||
|         strategies: [{ name: 'default' }], | ||||
|     }; | ||||
| 
 | ||||
|     const { value } = featureShema.validate(toggle); | ||||
|     t.deepEqual(value, toggle); | ||||
|     t.is(value.name, toggle.name); | ||||
| }); | ||||
| 
 | ||||
| test('should strip extra variant fields', t => { | ||||
| @ -52,6 +50,7 @@ test('should allow weightType=fix', t => { | ||||
|     const toggle = { | ||||
|         name: 'app.name', | ||||
|         type: 'release', | ||||
|         project: 'default', | ||||
|         enabled: false, | ||||
|         stale: false, | ||||
|         strategies: [{ name: 'default' }], | ||||
| @ -95,6 +94,7 @@ test('should be possible to define variant overrides', t => { | ||||
|     const toggle = { | ||||
|         name: 'app.name', | ||||
|         type: 'release', | ||||
|         project: 'some', | ||||
|         enabled: false, | ||||
|         stale: false, | ||||
|         strategies: [{ name: 'default' }], | ||||
| @ -152,6 +152,7 @@ test('should keep constraints', t => { | ||||
|     const toggle = { | ||||
|         name: 'app.constraints', | ||||
|         type: 'release', | ||||
|         project: 'default', | ||||
|         enabled: false, | ||||
|         stale: false, | ||||
|         strategies: [ | ||||
|  | ||||
| @ -31,6 +31,7 @@ const handleErrors = (res, logger, error) => { | ||||
|     switch (error.name) { | ||||
|         case 'NotFoundError': | ||||
|             return res.status(404).end(); | ||||
|         case 'InvalidOperationError': | ||||
|         case 'NameExistsError': | ||||
|             return res | ||||
|                 .status(409) | ||||
|  | ||||
| @ -7,18 +7,20 @@ const getApp = require('./app'); | ||||
| 
 | ||||
| const { startMonitoring } = require('./metrics'); | ||||
| const { createStores } = require('./db'); | ||||
| const { createServices } = require('./services'); | ||||
| const { createOptions } = require('./options'); | ||||
| const StateService = require('./services/state-service'); | ||||
| const User = require('./user'); | ||||
| const permissions = require('./permissions'); | ||||
| const AuthenticationRequired = require('./authentication-required'); | ||||
| const { addEventHook } = require('./event-hook'); | ||||
| const eventType = require('./event-type'); | ||||
| 
 | ||||
| async function createApp(options) { | ||||
|     // Database dependencies (stateful)
 | ||||
|     const logger = options.getLogger('server-impl.js'); | ||||
|     const eventBus = new EventEmitter(); | ||||
|     const stores = createStores(options, eventBus); | ||||
|     const services = createServices(stores, options, eventBus); | ||||
|     const secret = await stores.settingStore.get('unleash.secret'); | ||||
| 
 | ||||
|     const config = { | ||||
| @ -37,7 +39,8 @@ async function createApp(options) { | ||||
|         addEventHook(config.eventHook, stores.eventStore); | ||||
|     } | ||||
| 
 | ||||
|     const stateService = new StateService(config); | ||||
|     // TODO: refactor this. Should only be accessable via services object
 | ||||
|     const { stateService } = services; | ||||
|     config.stateService = stateService; | ||||
|     if (config.importFile) { | ||||
|         await stateService.importFile({ | ||||
| @ -53,6 +56,7 @@ async function createApp(options) { | ||||
|             app, | ||||
|             config, | ||||
|             stores, | ||||
|             services, | ||||
|             eventBus, | ||||
|             stateService, | ||||
|         }; | ||||
| @ -101,4 +105,5 @@ module.exports = { | ||||
|     User, | ||||
|     AuthenticationRequired, | ||||
|     permissions, | ||||
|     eventType, | ||||
| }; | ||||
|  | ||||
							
								
								
									
										7
									
								
								lib/services/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/services/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| const ProjectService = require('./project-service'); | ||||
| const StateService = require('./state-service'); | ||||
| 
 | ||||
| module.exports.createServices = (stores, config) => ({ | ||||
|     projectService: new ProjectService(stores, config), | ||||
|     stateService: new StateService(stores, config), | ||||
| }); | ||||
							
								
								
									
										17
									
								
								lib/services/project-schema.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/services/project-schema.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| const joi = require('joi'); | ||||
| const { nameType } = require('../routes/admin-api/util'); | ||||
| 
 | ||||
| const projectSchema = joi | ||||
|     .object() | ||||
|     .keys({ | ||||
|         id: nameType, | ||||
|         name: joi.string().required(), | ||||
|         description: joi | ||||
|             .string() | ||||
|             .allow(null) | ||||
|             .allow('') | ||||
|             .optional(), | ||||
|     }) | ||||
|     .options({ allowUnknown: false, stripUnknown: true }); | ||||
| 
 | ||||
| module.exports = projectSchema; | ||||
							
								
								
									
										93
									
								
								lib/services/project-service.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								lib/services/project-service.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| const NameExistsError = require('../error/name-exists-error'); | ||||
| const InvalidOperationError = require('../error/invalid-operation-error'); | ||||
| const eventType = require('../event-type'); | ||||
| const { nameType } = require('../routes/admin-api/util'); | ||||
| const schema = require('./project-schema'); | ||||
| 
 | ||||
| class ProjectService { | ||||
|     constructor( | ||||
|         { projectStore, eventStore, featureToggleStore }, | ||||
|         { getLogger }, | ||||
|     ) { | ||||
|         this.projectStore = projectStore; | ||||
|         this.eventStore = eventStore; | ||||
|         this.featureToggleStore = featureToggleStore; | ||||
|         this.logger = getLogger('services/project-service.js'); | ||||
|     } | ||||
| 
 | ||||
|     async getProjects() { | ||||
|         return this.projectStore.getAll(); | ||||
|     } | ||||
| 
 | ||||
|     async getProject(id) { | ||||
|         return this.projectStore.get(id); | ||||
|     } | ||||
| 
 | ||||
|     async createProject(newProject, username) { | ||||
|         const data = await schema.validateAsync(newProject); | ||||
|         await this.validateUniqueId(data); | ||||
|         await this.eventStore.store({ | ||||
|             type: eventType.PROJECT_CREATED, | ||||
|             createdBy: username, | ||||
|             data, | ||||
|         }); | ||||
|         await this.projectStore.create(data); | ||||
|     } | ||||
| 
 | ||||
|     async updateProject(updatedProject, username) { | ||||
|         await this.projectStore.get(updatedProject.id); | ||||
|         const project = await schema.validateAsync(updatedProject); | ||||
|         await this.eventStore.store({ | ||||
|             type: eventType.PROJECT_UPDATED, | ||||
|             createdBy: username, | ||||
|             data: project, | ||||
|         }); | ||||
|         await this.projectStore.update(project); | ||||
|     } | ||||
| 
 | ||||
|     async deleteProject(id, username) { | ||||
|         if (id === 'default') { | ||||
|             throw new InvalidOperationError( | ||||
|                 'You can not delete the default project!', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const toggles = await this.featureToggleStore.getFeaturesBy({ | ||||
|             project: id, | ||||
|             archived: 0, | ||||
|         }); | ||||
| 
 | ||||
|         if (toggles.length > 0) { | ||||
|             throw new InvalidOperationError( | ||||
|                 'You can not delete as project with active feature toggles', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         await this.eventStore.store({ | ||||
|             type: eventType.PROJECT_DELETED, | ||||
|             createdBy: username, | ||||
|             data: { id }, | ||||
|         }); | ||||
|         await this.projectStore.delete(id); | ||||
|     } | ||||
| 
 | ||||
|     async validateId(id) { | ||||
|         await nameType.validateAsync(id); | ||||
|         await this.validateUniqueId(id); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     async validateUniqueId(id) { | ||||
|         try { | ||||
|             await this.projectStore.hasProject(id); | ||||
|         } catch (error) { | ||||
|             // No conflict, everything ok!
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Interntional throw here!
 | ||||
|         throw new NameExistsError('A project with this id already exists.'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = ProjectService; | ||||
| @ -14,7 +14,7 @@ const { | ||||
| } = require('./state-util'); | ||||
| 
 | ||||
| class StateService { | ||||
|     constructor({ stores, getLogger }) { | ||||
|     constructor(stores, { getLogger }) { | ||||
|         this.eventStore = stores.eventStore; | ||||
|         this.toggleStore = stores.featureToggleStore; | ||||
|         this.strategyStore = stores.strategyStore; | ||||
|  | ||||
| @ -15,7 +15,10 @@ const { | ||||
| 
 | ||||
| function getSetup() { | ||||
|     const stores = store.createStores(); | ||||
|     return { stateService: new StateService({ stores, getLogger }), stores }; | ||||
|     return { | ||||
|         stateService: new StateService(stores, { getLogger }), | ||||
|         stores, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| test('should import a feature', async t => { | ||||
|  | ||||
							
								
								
									
										34
									
								
								migrations/20200928194947-add-projects.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								migrations/20200928194947-add-projects.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| /* eslint camelcase: "off" */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const async = require('async'); | ||||
| 
 | ||||
| exports.up = function(db, cb) { | ||||
|     async.series( | ||||
|         [ | ||||
|             db.createTable.bind(db, 'projects', { | ||||
|                 id: { | ||||
|                     type: 'string', | ||||
|                     length: 255, | ||||
|                     primaryKey: true, | ||||
|                     notNull: true, | ||||
|                 }, | ||||
|                 name: { type: 'string', notNull: true }, | ||||
|                 description: { type: 'string' }, | ||||
|                 created_at: { type: 'timestamp', defaultValue: 'now()' }, | ||||
|             }), | ||||
|             db.runSql.bind( | ||||
|                 db, | ||||
|                 ` | ||||
|               INSERT INTO projects(id, name, description) VALUES('default', 'Default', 'Default project'); | ||||
|               `,
 | ||||
|             ), | ||||
|         ], | ||||
|         cb, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| exports.down = function(db, cb) { | ||||
|     return db.dropTable('feature_types', cb); | ||||
| }; | ||||
							
								
								
									
										17
									
								
								migrations/20200928195238-add-project-id-to-features.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								migrations/20200928195238-add-project-id-to-features.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| exports.up = function(db, cb) { | ||||
|     return db.addColumn( | ||||
|         'features', | ||||
|         'project', | ||||
|         { | ||||
|             type: 'string', | ||||
|             defaultValue: 'default', | ||||
|         }, | ||||
|         cb, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| exports.down = function(db, cb) { | ||||
|     return db.removeColumn('features', 'project', cb); | ||||
| }; | ||||
| @ -75,7 +75,7 @@ | ||||
|     "express": "^4.17.1", | ||||
|     "gravatar-url": "^3.1.0", | ||||
|     "helmet": "^4.1.0", | ||||
|     "joi": "^17.2.0", | ||||
|     "joi": "^17.3.0", | ||||
|     "js-yaml": "^3.14.0", | ||||
|     "knex": "0.21.5", | ||||
|     "log4js": "^6.0.0", | ||||
|  | ||||
| @ -24,6 +24,7 @@ async function resetDatabase(stores) { | ||||
|         stores.db('client_instances').del(), | ||||
|         stores.db('context_fields').del(), | ||||
|         stores.db('users').del(), | ||||
|         stores.db('projects').del(), | ||||
|     ]); | ||||
| } | ||||
| 
 | ||||
| @ -43,6 +44,10 @@ function createClientInstance(store) { | ||||
|     return dbState.clientInstances.map(i => store.insert(i)); | ||||
| } | ||||
| 
 | ||||
| function createProjects(store) { | ||||
|     return dbState.projects.map(i => store.create(i)); | ||||
| } | ||||
| 
 | ||||
| function createFeatures(store) { | ||||
|     return dbState.features.map(f => store._createFeature(f)); | ||||
| } | ||||
| @ -54,6 +59,7 @@ async function setupDatabase(stores) { | ||||
|     updates.push(...createFeatures(stores.featureToggleStore)); | ||||
|     updates.push(...createClientInstance(stores.clientInstanceStore)); | ||||
|     updates.push(...createApplications(stores.clientApplicationsStore)); | ||||
|     updates.push(...createProjects(stores.projectStore)); | ||||
| 
 | ||||
|     await Promise.all(updates); | ||||
| } | ||||
|  | ||||
| @ -149,5 +149,11 @@ | ||||
|         { "name": "new", "weight": 50 } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "projects": [ | ||||
|     { | ||||
|       "id": "default", | ||||
|       "name": "Default" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | ||||
| @ -19,7 +19,7 @@ function createApp(stores, adminAuthentication = 'none', preHook) { | ||||
|         adminAuthentication, | ||||
|         secret: 'super-secret', | ||||
|         sessionAge: 4000, | ||||
|         stateService: new StateService({ stores, getLogger }), | ||||
|         stateService: new StateService(stores, { getLogger }), | ||||
|         getLogger, | ||||
|     }); | ||||
| } | ||||
|  | ||||
							
								
								
									
										148
									
								
								test/e2e/serices/project-service.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								test/e2e/serices/project-service.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | ||||
| const test = require('ava'); | ||||
| const dbInit = require('../helpers/database-init'); | ||||
| const getLogger = require('../../fixtures/no-logger'); | ||||
| const ProjectService = require('../../../lib/services/project-service'); | ||||
| 
 | ||||
| let stores; | ||||
| // let projectStore;
 | ||||
| let projectService; | ||||
| 
 | ||||
| test.before(async () => { | ||||
|     const db = await dbInit('project_service_serial', getLogger); | ||||
|     stores = db.stores; | ||||
|     // projectStore = stores.projectStore;
 | ||||
|     projectService = new ProjectService(stores, { getLogger }); | ||||
| }); | ||||
| 
 | ||||
| test.after(async () => { | ||||
|     await stores.db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should have default project', async t => { | ||||
|     const project = await projectService.getProject('default'); | ||||
|     t.assert(project); | ||||
|     t.is(project.id, 'default'); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should list all projects', async t => { | ||||
|     const project = { | ||||
|         id: 'test-list', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectService.createProject(project, 'someUser'); | ||||
|     const projects = await projectService.getProjects(); | ||||
|     t.is(projects.length, 2); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should create new project', async t => { | ||||
|     const project = { | ||||
|         id: 'test', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectService.createProject(project, 'someUser'); | ||||
|     const ret = await projectService.getProject('test'); | ||||
|     t.deepEqual(project.id, ret.id); | ||||
|     t.deepEqual(project.name, ret.name); | ||||
|     t.deepEqual(project.description, ret.description); | ||||
|     t.truthy(ret.createdAt); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should delete project', async t => { | ||||
|     const project = { | ||||
|         id: 'test-delete', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectService.createProject(project, 'some-user'); | ||||
|     await projectService.deleteProject(project.id, 'some-user'); | ||||
| 
 | ||||
|     try { | ||||
|         await projectService.getProject(project.id); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'No project found'); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should not be able to delete project with toggles', async t => { | ||||
|     const project = { | ||||
|         id: 'test-delete-with-toggles', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectService.createProject(project, 'some-user'); | ||||
|     await stores.featureToggleStore._createFeature({ | ||||
|         name: 'test-project-delete', | ||||
|         project: project.id, | ||||
|         enabled: false, | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|         await projectService.deleteProject(project.id, 'some-user'); | ||||
|     } catch (err) { | ||||
|         t.is( | ||||
|             err.message, | ||||
|             'You can not delete as project with active feature toggles', | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should not delete "default" project', async t => { | ||||
|     try { | ||||
|         await projectService.deleteProject('default', 'some-user'); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'You can not delete the default project!'); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should validate name, legal', async t => { | ||||
|     const result = await projectService.validateId('new_name'); | ||||
|     t.true(result); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should require URL friendly ID', async t => { | ||||
|     try { | ||||
|         await projectService.validateId('new name øæå'); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, '"value" must be URL friendly'); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should require unique ID', async t => { | ||||
|     try { | ||||
|         await projectService.validateId('default'); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'A project with this id already exists.'); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should update project', async t => { | ||||
|     const project = { | ||||
|         id: 'test-update', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
| 
 | ||||
|     const updatedProject = { | ||||
|         id: 'test-update', | ||||
|         name: 'New name', | ||||
|         description: 'Blah longer desc', | ||||
|     }; | ||||
| 
 | ||||
|     await projectService.createProject(project, 'some-user'); | ||||
|     await projectService.updateProject(updatedProject, 'some-user'); | ||||
| 
 | ||||
|     const readProject = await projectService.getProject(project.id); | ||||
| 
 | ||||
|     t.is(updatedProject.name, readProject.name); | ||||
|     t.is(updatedProject.description, readProject.description); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should give error when getting unkown project', async t => { | ||||
|     try { | ||||
|         await projectService.getProject('unknown'); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'No project found'); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										84
									
								
								test/e2e/stores/project-store.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								test/e2e/stores/project-store.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const test = require('ava'); | ||||
| const dbInit = require('../helpers/database-init'); | ||||
| const getLogger = require('../../fixtures/no-logger'); | ||||
| 
 | ||||
| let stores; | ||||
| let projectStore; | ||||
| 
 | ||||
| test.before(async () => { | ||||
|     const db = await dbInit('project_store_serial', getLogger); | ||||
|     stores = db.stores; | ||||
|     projectStore = stores.projectStore; | ||||
| }); | ||||
| 
 | ||||
| test.after(async () => { | ||||
|     await stores.db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should have default project', async t => { | ||||
|     const project = await projectStore.get('default'); | ||||
|     t.assert(project); | ||||
|     t.is(project.id, 'default'); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should create new project', async t => { | ||||
|     const project = { | ||||
|         id: 'test', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectStore.create(project); | ||||
|     const ret = await projectStore.get('test'); | ||||
|     t.deepEqual(project.id, ret.id); | ||||
|     t.deepEqual(project.name, ret.name); | ||||
|     t.deepEqual(project.description, ret.description); | ||||
|     t.truthy(ret.createdAt); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should delete project', async t => { | ||||
|     const project = { | ||||
|         id: 'test-delete', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
|     await projectStore.create(project); | ||||
|     await projectStore.delete(project.id); | ||||
| 
 | ||||
|     try { | ||||
|         await projectStore.get(project.id); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'No project found'); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| test.serial('should update project', async t => { | ||||
|     const project = { | ||||
|         id: 'test-update', | ||||
|         name: 'New project', | ||||
|         description: 'Blah', | ||||
|     }; | ||||
| 
 | ||||
|     const updatedProject = { | ||||
|         id: 'test-update', | ||||
|         name: 'New name', | ||||
|         description: 'Blah longer desc', | ||||
|     }; | ||||
| 
 | ||||
|     await projectStore.create(project); | ||||
|     await projectStore.update(updatedProject); | ||||
| 
 | ||||
|     const readProject = await projectStore.get(project.id); | ||||
| 
 | ||||
|     t.is(updatedProject.name, readProject.name); | ||||
|     t.is(updatedProject.description, readProject.description); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should give error when getting unkown project', async t => { | ||||
|     try { | ||||
|         await projectStore.get('unknown'); | ||||
|     } catch (err) { | ||||
|         t.is(err.message, 'No project found'); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										48
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -183,28 +183,11 @@ | ||||
|   dependencies: | ||||
|     arrify "^1.0.1" | ||||
| 
 | ||||
| "@hapi/address@^4.1.0": | ||||
|   version "4.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" | ||||
|   integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== | ||||
|   dependencies: | ||||
|     "@hapi/hoek" "^9.0.0" | ||||
| 
 | ||||
| "@hapi/formula@^2.0.0": | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128" | ||||
|   integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A== | ||||
| 
 | ||||
| "@hapi/hoek@^9.0.0": | ||||
|   version "9.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010" | ||||
|   integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw== | ||||
| 
 | ||||
| "@hapi/pinpoint@^2.0.0": | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" | ||||
|   integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw== | ||||
| 
 | ||||
| "@hapi/topo@^5.0.0": | ||||
|   version "5.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" | ||||
| @ -277,6 +260,23 @@ | ||||
|   dependencies: | ||||
|     "@passport-next/passport-strategy" "1.x.x" | ||||
| 
 | ||||
| "@sideway/address@^4.1.0": | ||||
|   version "4.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d" | ||||
|   integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA== | ||||
|   dependencies: | ||||
|     "@hapi/hoek" "^9.0.0" | ||||
| 
 | ||||
| "@sideway/formula@^3.0.0": | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" | ||||
|   integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== | ||||
| 
 | ||||
| "@sideway/pinpoint@^2.0.0": | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" | ||||
|   integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== | ||||
| 
 | ||||
| "@sindresorhus/is@^0.14.0": | ||||
|   version "0.14.0" | ||||
|   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" | ||||
| @ -3113,16 +3113,16 @@ istanbul-reports@^3.0.2: | ||||
|     html-escaper "^2.0.0" | ||||
|     istanbul-lib-report "^3.0.0" | ||||
| 
 | ||||
| joi@^17.2.0: | ||||
|   version "17.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.0.tgz#81cba6c1145130482d57b6d50129c7ab0e7d8b0a" | ||||
|   integrity sha512-9ZC8pMSitNlenuwKARENBGVvvGYHNlwWe5rexo2WxyogaxCB5dNHAgFA1BJQ6nsJrt/jz1p5vSqDT6W6kciDDw== | ||||
| joi@^17.3.0: | ||||
|   version "17.3.0" | ||||
|   resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" | ||||
|   integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== | ||||
|   dependencies: | ||||
|     "@hapi/address" "^4.1.0" | ||||
|     "@hapi/formula" "^2.0.0" | ||||
|     "@hapi/hoek" "^9.0.0" | ||||
|     "@hapi/pinpoint" "^2.0.0" | ||||
|     "@hapi/topo" "^5.0.0" | ||||
|     "@sideway/address" "^4.1.0" | ||||
|     "@sideway/formula" "^3.0.0" | ||||
|     "@sideway/pinpoint" "^2.0.0" | ||||
| 
 | ||||
| js-string-escape@^1.0.1: | ||||
|   version "1.0.1" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user