Skip to content

Commit

Permalink
Refactor JiraService to AddonService and fix IoC
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcelo Mendonça committed Jun 11, 2019
1 parent d6b7b85 commit f69960e
Show file tree
Hide file tree
Showing 32 changed files with 392 additions and 277 deletions.
2 changes: 1 addition & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
[strict]

[version]
0.93.0
0.100.0
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ This boilerplate creates a dependency injection container named `ioc` and adds i
* `api/src/adapters` for db and redis connections: `getAdapter('adapter-file-name')`
* `api/src/domains` for domain classes for business logic `getDomain('domain-file-name')`
* `api/src/models` for sequelize data models `getModel('model-file-name')`
* `api/src/services` for external integrations `getService('service-file-name')`
* `api/src/services` for external integrations `getService('service-file-name')`.

The `req` object also gets an object named `ace` which is a per-request authenticated atlas-connect-express plugin instance used by the included `JiraService` to perform api calls to the remote jira instance.
`AddonService` is a wrapper around `ace.httpClient` defined in `api/src/factories/AddonServiceFactory.js` and it gets loaded into the IoC container in the `addon.js` middleware function, due to its per-request nature.

ACE's middleware also includes a `context` object in `req` which contains [jira's context data](https://bitbucket.org/atlassian/atlassian-connect-express). In essence, the following variables are added to express' `req` object:
```js
Expand All @@ -83,8 +83,8 @@ ACE's middleware also includes a `context` object in `req` which contains [jira'
`api/entrypoint.sh` runs a database migration prior to launching the stack.
DB Migrations are created using [knex](http://knexjs.org/) and the node script `db` is an alias to it.
## JiraService
The JiraService instance you get from `req.ioc.getService('Jira')` gives you `get`, `post` and `put` methods that proxy `ace`'s httpClient methods. There are utility methods in `api/src/services/utils/jiraServiceUtils`:
## AddonService
The AddonService instance you get from `req.ioc.getService('Addon')` gives you `get`, `post` and `put` methods that proxy `ace`'s httpClient methods. There are utility methods in `api/src/services/utils/addonServiceUtils`:
* `getBodyJson`: returns jira api response body as an object
* `getResultWithoutPagination`: Paginates through jira's response until there are no more records left and returns the accumulated responses as an object
Expand Down
7 changes: 6 additions & 1 deletion api/atlassian-connect.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
"lifecycle": {
"installed": "/installed"
},
"scopes": ["READ", "WRITE", "ADMIN", "ACT_AS_USER"],
"scopes": [
"READ",
"WRITE",
"ADMIN",
"ACT_AS_USER"
],
"modules": {
"generalPages": [
{
Expand Down
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"pg-pool": "^2.0.6",
"reflect-metadata": "^0.1.13",
"regenerator-runtime": "^0.13.2",
"request-promise": "^4.2.4",
"request-promise-native": "^1.0.7",
"sequelize": "^5.8.6",
"sleep-ms": "^2.0.1",
"uuid": "^3.3.2"
Expand All @@ -63,7 +63,7 @@
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-flowtype": "^3.9.1",
"eslint-plugin-import": "^2.17.2",
"flow-bin": "^0.98.1",
"flow-bin": "^0.100.0",
"flow-typed": "^2.5.2",
"graphql-cli": "^3.0.11",
"knex": "^0.16.5",
Expand Down
16 changes: 7 additions & 9 deletions api/src/domains/JiraIssue.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// @flow
import { helpers } from 'inversify-vanillajs-helpers';
import 'reflect-metadata';
import type { JiraServiceInterface as JiraService } from '../types';
import type { AddonServiceInterface as AddonService } from '../types';
import { service } from '../ioc/utils';
import { getBodyJson } from '../services/utils/jiraServiceUtils';

export type JiraIssue = {
id: string,
Expand All @@ -17,26 +16,25 @@ export interface JiraIssueDomainInterface {
}

class JiraIssueDomain implements JiraIssueDomainInterface {
jiraService: JiraService;
addonService: AddonService;

constructor(jiraService: JiraService) {
this.jiraService = jiraService;
constructor(addonService: AddonService) {
this.addonService = addonService;
}

async getIssues(startAt: number, maxResults: number): Promise<Array<?JiraIssue>> {
const response = await this.jiraService.get(`/rest/api/2/search?startAt=${startAt}&maxResults=${maxResults}`);
const { issues } = JSON.parse(response);
const { issues } = await this.addonService.get(`/rest/api/3/search?startAt=${startAt}&maxResults=${maxResults}`);
return issues;
}

updateSummary(id: string, summary: string): Promise<JiraIssue> {
return this.jiraService.put({
return this.addonService.put({
url: `/rest/api/2/issue/${id}`,
body: { fields: { summary } },
json: true,
});
}
}
helpers.annotate(JiraIssueDomain, [service('Jira')]);
helpers.annotate(JiraIssueDomain, [service('Addon')]);

export default JiraIssueDomain;
12 changes: 6 additions & 6 deletions api/src/domains/__tests__/JiraIssue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ chai.use(sinonChai);
const sandbox = createSandbox();

describe('JiraIssue Domain', () => {
const jiraServiceMock = {
const addonServiceMock = {
get: sandbox.stub().resolves(JSON.stringify({ issues: [{ id: 1 }] })),
put: sandbox.stub().resolves(true),
};
const underTest = new JiraIssueDomain(jiraServiceMock);
const underTest = new JiraIssueDomain(addonServiceMock);
beforeEach(() => {});

afterEach(() => {
Expand All @@ -20,17 +20,17 @@ describe('JiraIssue Domain', () => {
describe('getIssues', () => {
it('delegates to jira service', async () => {
await underTest.getIssues(1, 10);
expect(jiraServiceMock.get).to.have.been.calledOnce;
expect(jiraServiceMock.get.getCalls()[0].args[0])
expect(addonServiceMock.get).to.have.been.calledOnce;
expect(addonServiceMock.get.getCalls()[0].args[0])
.to.match(/startAt=1/)
.and.match(/maxResults=10/);
});
});
describe('udpateSummary', () => {
it('delegates to jira service', async () => {
await underTest.updateSummary('id-1', 'new summary');
expect(jiraServiceMock.put).to.have.been.calledOnce;
expect(jiraServiceMock.put.getCalls()[0].args[0]).to.deep.include({
expect(addonServiceMock.put).to.have.been.calledOnce;
expect(addonServiceMock.put.getCalls()[0].args[0]).to.deep.include({
body: { fields: { summary: 'new summary' } },
});
});
Expand Down
52 changes: 52 additions & 0 deletions api/src/factories/AddonService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @flow
import { promisify } from 'util';
import { getBodyJson } from '../utils/addonService';
import type { AddonServiceInterface, JiraApiResponse, ACEHttp } from '../types';

/**
* Wrapper around Jira's HTTP Client
* https://developer.atlassian.com/cloud/jira/platform/rest/v2/
*
* @export
* @class AddonService
*/
export default class AddonService implements AddonServiceInterface {
_http: ACEHttp;

_put: (...args: any) => Promise<any>;

_post: (...args: any) => Promise<any>;

_get: (...args: any) => Promise<any>;

_del: (...args: any) => Promise<any>;

async get(...args: any): Promise<JiraApiResponse> {
return getBodyJson(this._get(...args));
}

async put(...args: any): Promise<JiraApiResponse> {
return getBodyJson(this._put(...args));
}

async post(...args: any): Promise<JiraApiResponse> {
return getBodyJson(this._post(...args));
}

async _del(...args: any): Promise<JiraApiResponse> {
return getBodyJson(this._del(...args));
}

constructor(http: ACEHttp) {
this._http = http;
// promisify ace's get and post functions
this._get = promisify(this._http.get).bind(this._http);
this._put = promisify(this._http.put).bind(this._http);
this._post = promisify(this._http.post).bind(this._http);
this._del = promisify(this._http.del).bind(this._http);
}

asUserByAccountId(userAccountId: string): Function {
return this._http.asUserByAccountId(userAccountId);
}
}
6 changes: 6 additions & 0 deletions api/src/factories/AddonServiceFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @flow
import AddonService from './AddonService';
import type { ACEHttp } from '../types';


export default (http: ACEHttp) => new AddonService(http);
36 changes: 36 additions & 0 deletions api/src/factories/__tests__/AddonService.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createSandbox } from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import AddonService from '../AddonService';

chai.use(sinonChai);
const sandbox = createSandbox();

describe('JiraIssue Domain', () => {
const response = { body: JSON.stringify({ foo: 'bar' }) };
const httpMock = {
get: (args, cb) => cb(null, response),
put: (args, cb) => cb(null, response),
post: (args, cb) => cb(null, response),
del: (args, cb) => cb(null, response),
};
const underTest = new AddonService(httpMock);
beforeEach(() => { });

afterEach(() => {
sandbox.restore();
});

describe('getIssues', () => {
it('parses response body', async () => {
const getResponse = await underTest.get('/nowhere');
const putResponse = await underTest.get('/nowhere');
const postResponse = await underTest.get('/nowhere');
const delResponse = await underTest.get('/nowhere');
expect(getResponse).to.be.eql({ foo: 'bar' });
expect(putResponse).to.be.eql({ foo: 'bar' });
expect(postResponse).to.be.eql({ foo: 'bar' });
expect(delResponse).to.be.eql({ foo: 'bar' });
});
});
});
9 changes: 1 addition & 8 deletions api/src/ioc/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chai, { expect } from 'chai';
import { createSandbox } from 'sinon';
import sinonChai from 'sinon-chai';
import {
adapter, model, domain, serviceFactory, service,
adapter, model, domain, service,
} from '../utils';

chai.use(sinonChai);
Expand Down Expand Up @@ -36,13 +36,6 @@ describe('ioc/utils', () => {
});
});

describe('serviceFactory', () => {
it('appends ServiceFactory to source', () => {
const result = serviceFactory('test');
expect(result).to.equals('testServiceFactory');
});
});

describe('service', () => {
it('appends Service to source', () => {
const result = service('test');
Expand Down
Loading

0 comments on commit f69960e

Please sign in to comment.