diff --git a/migrations/20190129_organization_config.sql b/migrations/20190129_organization_config.sql index 3e9e041e..3e218c71 100644 --- a/migrations/20190129_organization_config.sql +++ b/migrations/20190129_organization_config.sql @@ -4,9 +4,9 @@ -- CREATE TABLE org_config ( id bigint NOT NULL, - orgId character varying(45) NOT NULL, - configName character varying(45) NOT NULL, - configValue character varying(512), + "orgId" character varying(45) NOT NULL, + "configName" character varying(45) NOT NULL, + "configValue" character varying(512), "deletedAt" timestamp with time zone, "createdAt" timestamp with time zone, "updatedAt" timestamp with time zone, diff --git a/package-lock.json b/package-lock.json index 20e07864..10c0fce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7602,7 +7602,7 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#02350d46d3b8d89ee4686d5c1a5d0086943cbfe8", + "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", "requires": { "auth0-js": "9.8.2", "axios": "0.12.0", @@ -7611,12 +7611,13 @@ "jwks-rsa": "1.3.0", "le_node": "1.8.0", "lodash": "4.17.11", - "millisecond": "0.1.2" + "millisecond": "0.1.2", + "request": "2.88.0" }, "dependencies": { "axios": { "version": "0.12.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.12.0.tgz", "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", "requires": { "follow-redirects": "0.0.7" @@ -7624,7 +7625,7 @@ }, "follow-redirects": { "version": "0.0.7", - "resolved": "http://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", "requires": { "debug": "2.6.9", diff --git a/postman.json b/postman.json index 56087004..8dd0d400 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "e810fc27-5518-4cc5-8f90-6b1423c6b0b4", + "_postman_id": "d9f6cae1-30c2-4c85-96c3-428b1f2fc08d", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -3303,173 +3303,209 @@ }, { "name": "Organization Config", - "description": "", "item": [ { "name": "Create organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "POST", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "{\r\n \"param\":{\r\n \"orgId\": \"20000013\",\r\n \"configName\": \"project_catalog_url\",\r\n \"configValue\": \"/projects/1\"\r\n }\r\n}" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig" + ] + } }, "response": [] }, { - "name": "List organization config", + "name": "List organization config - error without filter", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig", "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" - }, - "response": [] - }, - { - "name": "List organization config - filter", - "request": { "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "raw": "{{api-url}}/v4/projects/metadata/orgConfig", "host": [ "{{api-url}}" ], "path": [ "v4", + "projects", + "metadata", "orgConfig" - ], - "query": [ - { - "key": "filter", - "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", - "equals": true, - "description": "" - } - ], - "variable": [] - }, + ] + } + }, + "response": [] + }, + { + "name": "List organization config - filter", + "request": { "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "orgConfig" + ], + "query": [ + { + "key": "filter", + "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url" + } + ] + } }, "response": [] }, { "name": "Get organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "GET", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] }, { "name": "Update organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "PATCH", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "{\r\n \"param\":{\r\n \"configName\": \"project_catalog_url\"\r\n }\r\n}" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] }, { "name": "Delete organization config", "request": { - "url": "{{api-url}}/v4/projects/metadata/orgConfig/1", "method": "DELETE", "header": [ { "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "application/json" }, { "key": "Authorization", - "value": "Bearer {{jwt-token}}", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", "raw": "" }, - "description": "" + "url": { + "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "orgConfig", + "1" + ] + } }, "response": [] } @@ -5184,4 +5220,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/routes/orgConfig/list.js b/src/routes/orgConfig/list.js index 55f05f1d..0c8222f0 100644 --- a/src/routes/orgConfig/list.js +++ b/src/routes/orgConfig/list.js @@ -1,17 +1,30 @@ /** * API to list organization config */ +import validate from 'express-validation'; +import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; const permissions = tcMiddleware.permissions; +const schema = { + query: { + filter: Joi.string().required(), + }, +}; + module.exports = [ + validate(schema), permissions('orgConfig.view'), (req, res, next) => { // handle filters const filters = util.parseQueryFilter(req.query.filter); + // Throw error if orgId is not present in filter + if (!filters.orgId) { + return next(util.buildApiError('Missing filter orgId', 422)); + } if (!util.isValidFilter(filters, ['orgId', 'configName'])) { return util.handleError('Invalid filters', null, req, next); } diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index e7f2ac1e..44f42cc7 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -32,40 +32,12 @@ describe('LIST organization config', () => { ]; beforeEach(() => testUtil.clearDb() - .then(() => models.OrgConfig.create(configs[0])) - .then(() => models.OrgConfig.create(configs[1])) - .then(() => Promise.resolve()), + .then(() => models.OrgConfig.bulkCreate(configs)), ); after(testUtil.clearDb); describe('GET /orgConfig', () => { - it('should return 200 for admin', (done) => { - request(server) - .get(`${orgConfigPath}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const config = configs[0]; - - const resJson = res.body.result.content; - resJson.should.have.length(2); - resJson[0].id.should.be.eql(config.id); - resJson[0].orgId.should.be.eql(config.orgId); - resJson[0].configName.should.be.eql(config.configName); - resJson[0].configValue.should.be.eql(config.configValue); - should.exist(resJson[0].createdAt); - resJson[0].updatedBy.should.be.eql(config.updatedBy); - should.exist(resJson[0].updatedAt); - should.not.exist(resJson[0].deletedBy); - should.not.exist(resJson[0].deletedAt); - - done(); - }); - }); - - it('should return 200 with filters', (done) => { + it('should return 200 for admin with filter', (done) => { request(server) .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ @@ -91,15 +63,15 @@ describe('LIST organization config', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 200 even if user is not authenticated with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .expect(200, done); }); - it('should return 200 for connect admin', (done) => { + it('should return 200 for connect admin with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) @@ -107,9 +79,9 @@ describe('LIST organization config', () => { .end(done); }); - it('should return 200 for connect manager', (done) => { + it('should return 200 for connect manager with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.manager}`, }) @@ -117,22 +89,34 @@ describe('LIST organization config', () => { .end(done); }); - it('should return 200 for member', (done) => { + it('should return 200 for member with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(200, done); }); - it('should return 200 for copilot', (done) => { + it('should return 200 for copilot with filter', (done) => { request(server) - .get(`${orgConfigPath}`) + .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .expect(200, done); }); + + it('should return 422 without filter query param', (done) => { + request(server) + .get(`${orgConfigPath}`) + .expect(422, done); + }); + + it('should return 422 with filter query param but without orgId defined', (done) => { + request(server) + .get(`${orgConfigPath}?filter=configName=${configs[0].configName}`) + .expect(422, done); + }); }); }); diff --git a/swagger.yaml b/swagger.yaml index b547e16e..b87eb9e1 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -659,7 +659,7 @@ paths: description: Project migrated successfully schema: $ref: "#/definitions/ProjectUpgradeResponse" - + /projects/metadata: get: tags: @@ -1177,6 +1177,137 @@ paths: '204': description: Project type successfully removed + /projects/metadata/orgConfig: + get: + tags: + - orgConfig + operationId: findOrgConfigs + security: + - Bearer: [] + description: Retrieve all organization configs. All user roles can access this endpoint. + parameters: + - name: filter + required: true + type: string + in: query + description: | + Url encoded list of Supported filters + - orgId (required) + - configName + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: A list of organization configs + schema: + $ref: "#/definitions/OrgConfigListResponse" + post: + tags: + - orgConfig + operationId: addOrgConfig + security: + - Bearer: [] + description: Create a organization config. Only admin or connect admin can access this endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/OrgConfigCreateBodyParam' + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '201': + description: Returns the newly created organization config + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/metadata/orgConfig/{id}: + get: + tags: + - orgConfig + description: Retrieve project type by id. All user roles can access this endpoint. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project type + schema: + $ref: "#/definitions/OrgConfigResponse" + parameters: + - $ref: "#/parameters/idParam" + operationId: getOrgConfig + patch: + tags: + - orgConfig + operationId: updateOrgConfig + security: + - Bearer: [] + description: Update a organization config. Only admin or connect admin can access this endpoint. + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: Successfully updated organization config. + schema: + $ref: "#/definitions/OrgConfigResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/idParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/OrgConfigCreateBodyParam" + + delete: + tags: + - orgConfig + description: Remove an existing organization config. Only admin or connect admin can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/idParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If organization config is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Organization config successfully removed /timelines: get: @@ -1741,7 +1872,6 @@ paths: description: Invalid server state or unknown error schema: $ref: "#/definitions/ErrorModel" - parameters: projectIdParam: name: projectId @@ -1812,6 +1942,13 @@ parameters: type: integer format: int64 minimum: 1 + idParam: + name: id + in: path + description: organization config id + required: true + type: integer + format: int64 offsetParam: name: offset description: "number of items to skip. Defaults to 0" @@ -3153,6 +3290,132 @@ definitions: readOnly: true - $ref: "#/definitions/ProjectTypeCreateRequest" + OrgConfigRequest: + title: Organization config request object + type: object + required: + - orgId + - configName + properties: + orgId: + type: string + description: the org id + configName: + type: string + description: the organization config name + + OrgConfigCreateRequest: + title: Organization config creation request object + type: object + allOf: + - type: object + properties: + configValue: + type: string + description: the organization config id + - $ref: "#/definitions/OrgConfigRequest" + + OrgConfigCreateBodyParam: + title: Organization config creation body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/OrgConfigCreateRequest" + + OrgConfig: + title: Organization config object + allOf: + - type: object + required: + - id + - orgId + - configName + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + orgId: + type: string + description: the org id + configName: + type: string + description: the config name + configValue: + type: string + description: the config value + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/OrgConfigCreateRequest" + + OrgConfigResponse: + title: Single organization config response object + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/OrgConfig" + + OrgConfigListResponse: + title: Organization confige list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/OrgConfig" ProjectTypeResponse: title: Single project type response object @@ -3584,7 +3847,7 @@ definitions: format: long minimum: 1 description: the milestone template reference id - metadata: + metadata: type: object description: the milestone template metadata @@ -3708,7 +3971,7 @@ definitions: type: array items: $ref: "#/definitions/MilestoneTemplate" - + AllMetadataResponse: title: All metadata response object type: object @@ -3752,7 +4015,7 @@ definitions: type: array items: $ref: "#/definitions/ProductCategory" - + ProjectMemberInvite: type: object properties: @@ -3797,7 +4060,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - + AddProjectMemberInvitesRequest: title: Add project member invites request object type: object @@ -3819,8 +4082,8 @@ definitions: role: description: The target role in the project type: string - enum: ["manager", "customer", "copilot"] - + enum: ["manager", "customer", "copilot"] + UpdateProjectMemberInviteRequest: title: Update project member invite request object type: object @@ -3839,7 +4102,7 @@ definitions: description: The invite status type: string enum: ["pending", "accepted", "refused", "canceled"] - + ProjectMemberInviteResponse: title: Project member invite response object type: object @@ -3861,4 +4124,3 @@ definitions: $ref: "#/definitions/ResponseMetadata" content: $ref: "#/definitions/ProjectMemberInvite" -