Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.1.0 #276

Merged
merged 14 commits into from
Jun 4, 2024
Merged

4.1.0 #276

Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ module.exports = {
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
...baseConfig.parserOptions,
"ecmaVersion": "latest",
"sourceType": "module"
"sourceType": "module",
},
"plugins": [
...baseConfig.plugins,
"@typescript-eslint",
"perfectionist",
"@typescript-eslint",
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ jobs:
run: npm install -g yarn
- name: Install dependencies
run: yarn install --immutable
- name: Run linter
run: npm run lint
- name: Run ESLint on changed files
uses: tj-actions/eslint-changed-files@v25
with:
token: ${{ secrets.GITHUB_TOKEN }}
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shortcut-api",
"version": "4.0.1",
"version": "4.1.1-alpha.0",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
Expand Down Expand Up @@ -28,8 +28,8 @@
"prepublishOnly": "npm run build",
"test": "jest",
"docs": "npx typedoc",
"lint": "eslint --ext ts src tests",
"lint:fix": "eslint . --ext .ts --fix"
"lint": "eslint --ext ts src",
"lint:fix": "eslint src --ext .ts --fix"
},
"keywords": [],
"author": "",
Expand All @@ -46,8 +46,9 @@
"@typescript-eslint/parser": "^7.5.0",
"axios-mock-adapter": "^1.22.0",
"eslint": "^8.57.0",
"eslint-config-yenz": "^2.0.1",
"eslint-config-yenz": "^5.2.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^28.5.0",
"eslint-plugin-perfectionist": "^2.7.0",
"jest": "^29.7.0",
"renamer": "^5.0.0",
Expand Down
36 changes: 23 additions & 13 deletions src/epics/epic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios from 'axios'
import axios, { AxiosError, AxiosResponse } from 'axios'

import BaseData from '@sx/base-data'
import BaseResource from '@sx/base-resource'
Expand All @@ -12,9 +12,9 @@ import TeamsService from '@sx/teams/teams-service'
import CreateThreadedCommentData from '@sx/threaded-comments/contracts/create-threaded-comment-data'
import ThreadedCommentApiData from '@sx/threaded-comments/contracts/threaded-comment-api-data'
import ThreadedCommentInterface from '@sx/threaded-comments/contracts/threaded-comment-interface'
import {convertApiFields, convertToApiFields} from '@sx/utils/convert-fields'
import {handleResponseFailure} from '@sx/utils/handle-response-failure'
import {getHeaders} from '@sx/utils/headers'
import { convertApiFields, convertToApiFields } from '@sx/utils/convert-fields'
import { handleResponseFailure } from '@sx/utils/handle-response-failure'
import { getHeaders } from '@sx/utils/headers'
import UUID from '@sx/utils/uuid'


Expand All @@ -36,7 +36,7 @@ export default class Epic extends BaseResource<EpicInterface> implements EpicInt
* @returns {Promise<Objective[]>}
*/
get objectives(): Promise<Objective[]> {
const service = new ObjectivesService({headers: getHeaders()})
const service = new ObjectivesService({ headers: getHeaders() })
return service.getMany(this.objectiveIds)
}

Expand All @@ -45,7 +45,7 @@ export default class Epic extends BaseResource<EpicInterface> implements EpicInt
* @returns {Promise<Team>}
*/
get teams(): Promise<Team[]> {
const service = new TeamsService({headers: getHeaders()})
const service = new TeamsService({ headers: getHeaders() })
return service.getMany(this.groupIds)
}

Expand All @@ -54,10 +54,19 @@ export default class Epic extends BaseResource<EpicInterface> implements EpicInt
* @returns {Promise<Member[]>}
*/
get followers(): Promise<Member[]> {
const service: MembersService = new MembersService({headers: getHeaders()})
const service: MembersService = new MembersService({ headers: getHeaders() })
return service.getMany(this.followerIds)
}

/**
* Get the owners of the epic
* @returns {Promise<Member[]>}
*/
get owners(): Promise<Member[]> {
const service = new MembersService({ headers: getHeaders() })
return service.getMany(this.ownerIds)
}

/**
* Add a comment to the epic authored by the user associated with the API key currently in use
*
Expand All @@ -72,14 +81,14 @@ export default class Epic extends BaseResource<EpicInterface> implements EpicInt
*/
public async comment(comment: string): Promise<ThreadedCommentInterface | void> {
const url = `${Epic.baseUrl}/${this.id}/comments`
const response = await axios.post(url, {text: comment}, {headers: getHeaders()}).catch((error) => {
handleResponseFailure(error, {text: comment})
const response: void | AxiosResponse<ThreadedCommentApiData, unknown> = await axios.post(url, { text: comment }, { headers: getHeaders() }).catch((error: AxiosError) => {
handleResponseFailure(error, { text: comment })
})
if (!response) {
throw new Error('Failed to add comment')
}
const data: ThreadedCommentApiData = response.data
return convertApiFields(data) as ThreadedCommentInterface
return convertApiFields<ThreadedCommentApiData, ThreadedCommentInterface>(data)
}

/**
Expand All @@ -94,17 +103,18 @@ export default class Epic extends BaseResource<EpicInterface> implements EpicInt
public async addComment(comment: CreateThreadedCommentData): Promise<ThreadedCommentInterface | void> {
const url = `${Epic.baseUrl}/${this.id}/comments`
const requestData: BaseData = convertToApiFields(comment)
const response = await axios.post(url, requestData, {headers: getHeaders()}).catch((error) => {
const response: void | AxiosResponse<ThreadedCommentApiData, unknown> = await axios.post(url, requestData, { headers: getHeaders() }).catch((error: AxiosError) => {
handleResponseFailure(error, requestData)
})
if (!response){
if (!response) {
throw new Error('Failed to add comment')
}
const data: ThreadedCommentApiData = response.data
return convertApiFields(data) as ThreadedCommentInterface
return convertApiFields<ThreadedCommentApiData, ThreadedCommentInterface>(data)
}



appUrl: string
archived: boolean
associatedGroups: []
Expand Down
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import ObjectiveInterface from '@sx/objectives/contracts/objective-interface'
import LabelInterface from '@sx/labels/contracts/label-interface'
import KeyResultInterface from '@sx/key-results/contracts/key-result-interface'
import LinkedFileInterface from '@sx/linked-files/contracts/linked-file-interface'
import MemberProfile from '@sx/members/contracts/member-profile'


// Utils
Expand All @@ -71,6 +72,8 @@ export {
LinkedFilesService,
CustomFieldsService,
UploadedFilesService,
}
export {
IterationInterface,
MemberInterface,
StoryInterface,
Expand All @@ -79,9 +82,8 @@ export {
EpicInterface,
ObjectiveInterface,
LabelInterface,
KeyResultInterface
}
export {
KeyResultInterface,
MemberProfile,
ThreadedCommentInterface,
CreateThreadedCommentData,
StoryCommentInterface,
Expand Down
19 changes: 10 additions & 9 deletions src/members/contracts/member-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import Workspace from '@sx/workspace/contracts/workspace'


interface MemberProfile extends BaseInterface {
deactivated: boolean,
displayIcon: string,
emailAddress: string,
gravatarHash: string,
id: UUID,
isOwner: boolean,
mentionName: string,
name: string,
deactivated: boolean
displayIcon: string
emailAddress: string
gravatarHash: string
id: UUID
isOwner: boolean
mentionName: string
name: string
twoFactorAuthEnabled: boolean
workspace: Workspace
}

export {MemberProfile}
export { MemberProfile }
export default MemberProfile
17 changes: 15 additions & 2 deletions src/stories/custom-fields/story-custom-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import CustomFieldsService from '@sx/custom-fields/custom-fields-service'
import StoryInterface from '@sx/stories/contracts/story-interface'
import StoryCustomFieldInterface from '@sx/stories/custom-fields/contracts/story-custom-field-interface'
import {getHeaders} from '@sx/utils/headers'

Check failure on line 6 in src/stories/custom-fields/story-custom-field.ts

View workflow job for this annotation

GitHub Actions / lint

[eslint] reported by reviewdog 🐶 A space is required after '{'. Raw Output: {"ruleId":"@stylistic/object-curly-spacing","severity":2,"message":"A space is required after '{'.","line":6,"column":8,"nodeType":"ImportDeclaration","messageId":"requireSpaceAfter","endLine":6,"endColumn":9,"fix":{"range":[356,356],"text":" "}}

Check failure on line 6 in src/stories/custom-fields/story-custom-field.ts

View workflow job for this annotation

GitHub Actions / lint

[eslint] reported by reviewdog 🐶 A space is required before '}'. Raw Output: {"ruleId":"@stylistic/object-curly-spacing","severity":2,"message":"A space is required before '}'.","line":6,"column":19,"nodeType":"ImportDeclaration","messageId":"requireSpaceBefore","endLine":6,"endColumn":20,"fix":{"range":[366,366],"text":" "}}
import UUID from '@sx/utils/uuid'


export default class StoryCustomField extends BaseResource<StoryInterface> implements StoryCustomFieldInterface {
public baseUrl = 'https://api.app.shortcut.com/api/v3/stories'
public availableOperations = []
private customField: CustomField | null = null

constructor(init: StoryCustomFieldInterface) {
super()
Expand All @@ -23,8 +24,20 @@
* without making a separate request.
*/
get field(): Promise<CustomField> {
const service = new CustomFieldsService({headers: getHeaders()})
return service.get(this.fieldId)
if (this.customField) {
return Promise.resolve(this.customField)
}
const service: CustomFieldsService = new CustomFieldsService({headers: getHeaders()})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@stylistic/object-curly-spacing> reported by reviewdog 🐶
A space is required after '{'.

Suggested change
const service: CustomFieldsService = new CustomFieldsService({headers: getHeaders()})
const service: CustomFieldsService = new CustomFieldsService({ headers: getHeaders()})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@stylistic/object-curly-spacing> reported by reviewdog 🐶
A space is required before '}'.

Suggested change
const service: CustomFieldsService = new CustomFieldsService({headers: getHeaders()})
const service: CustomFieldsService = new CustomFieldsService({headers: getHeaders() })

const field: Promise<CustomField> = service.get(this.fieldId)
field.then((field: CustomField) => this.customField = field)
return field
}

/**
* Get the name of the custom field that this story custom field is associated with.
*/
get name(): Promise<string> {
return this.field.then((field) => field.name)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@stylistic/arrow-parens> reported by reviewdog 🐶
Unexpected parentheses around single function argument having a body with no curly braces.

Suggested change
return this.field.then((field) => field.name)
return this.field.then(field => field.name)

}

fieldId: UUID
Expand Down
74 changes: 48 additions & 26 deletions tests/epics/epic.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import axios from 'axios'

import Epic from '../../src/epics/epic'
import Objective from '../../src/objectives/objective'
import ObjectivesService from '../../src/objectives/objectives-service'
import Team from '../../src/teams/team'
import TeamsService from '../../src/teams/teams-service'
import {convertApiFields} from '../../src/utils/convert-fields'
import {getHeaders} from '../../src/utils/headers'
import Epic from '@sx/epics/epic'
import Objective from '@sx/objectives/objective'
import ObjectivesService from '@sx/objectives/objectives-service'
import Team from '@sx/teams/team'
import TeamsService from '@sx/teams/teams-service'
import { convertApiFields } from '@sx/utils/convert-fields'
import { getHeaders } from '@sx/utils/headers'

import Member from '../../src/members/member'
import MembersService from '../../src/members/members-service'


jest.mock('axios', () => ({
Expand All @@ -25,53 +28,73 @@ describe('Epic', () => {
})

describe('objectives getter', () => {
it('returns an array of objectives', () => {
const objectives = [{id: 1}, {id: 2}]
it('returns an array of objectives', async () => {
const objectives = [{ id: 1 }, { id: 2 }]
jest.spyOn(ObjectivesService.prototype, 'getMany').mockResolvedValue(objectives as unknown as Objective[])
const epic = new Epic({id: 1, objectiveIds: [1, 2]})
expect(epic.objectives).resolves.toEqual([{id: 1}, {id: 2}])
const epic = new Epic({ id: 1, objectiveIds: [1, 2] })
await expect(epic.objectives).resolves.toEqual([{ id: 1 }, { id: 2 }])
expect(ObjectivesService.prototype.getMany).toHaveBeenCalledWith([1, 2])
})
})

describe('teams getter', () => {
it('returns the team object', () => {
const teams = [{id: 1}]
it('returns the team object', async () => {
const teams = [{ id: 1 }]
jest.spyOn(TeamsService.prototype, 'getMany').mockResolvedValue(teams as unknown as Team[])
const epic = new Epic({groupIds: [1]})
expect(epic.teams).resolves.toEqual([{id: 1}])
const epic = new Epic({ groupIds: [1] })
await expect(epic.teams).resolves.toEqual([{ id: 1 }])
expect(TeamsService.prototype.getMany).toHaveBeenCalledWith([1])
})
})

describe('followers getter', () => {
it('returns the followers', async () => {
const members = [{ id: 1 }]
jest.spyOn(MembersService.prototype, 'getMany').mockResolvedValue(members as unknown as Member[])
const epic = new Epic({ followerIds: [1] })
await expect(epic.followers).resolves.toEqual([{ id: 1 }])
expect(MembersService.prototype.getMany).toHaveBeenCalledWith([1])
})
})

describe('owners getter', () => {
it('returns the owner object', async () => {
const owners = [{ id: 1 }]
jest.spyOn(MembersService.prototype, 'getMany').mockResolvedValue(owners as unknown as Member[])
const epic = new Epic({ ownerIds: [1] })
await expect(epic.owners).resolves.toEqual([{ id: 1 }])
expect(MembersService.prototype.getMany).toHaveBeenCalledWith([1])
})
})

describe('comment method', () => {
it('successfully posts a comment and returns the epic comment object', async () => {
const commentData = {text: 'Test comment'}
const expectedResponse = {data: commentData}
const commentData = { text: 'Test comment' }
const expectedResponse = { data: commentData }
mockedAxios.post.mockResolvedValue(expectedResponse)

const epic = new Epic({id: 1})
const epic = new Epic({ id: 1 })
const result = await epic.comment('Test comment')

expect(result).toEqual(convertApiFields(commentData))
expect(mockedAxios.post).toHaveBeenCalledWith(`${Epic.baseUrl}/${epic.id}/comments`, {text: 'Test comment'}, {headers: getHeaders()})
expect(mockedAxios.post).toHaveBeenCalledWith(`${Epic.baseUrl}/${epic.id}/comments`, { text: 'Test comment' }, { headers: getHeaders() })
})

it('throws an error if the mockedAxios request fails', async () => {
mockedAxios.post.mockRejectedValue(new Error('Network error'))
const epic = new Epic({id: 1})
const epic = new Epic({ id: 1 })

await expect(epic.comment('Test comment')).rejects.toThrow('Failed to add comment')
})
})

describe('addComment method', () => {
it('successfully posts a comment and returns the epic comment object', async () => {
const commentData = {text: 'Test comment'}
const expectedResponse = {data: commentData}
const commentData = { text: 'Test comment' }
const expectedResponse = { data: commentData }
mockedAxios.post.mockResolvedValue(expectedResponse)

const epic = new Epic({id: 1})
const epic = new Epic({ id: 1 })
const comment = {
text: 'Test comment',
authorId: '123',
Expand All @@ -88,13 +111,12 @@ describe('Epic', () => {
created_at: null,
external_id: null,
updated_at: null
}, {headers: getHeaders()})

}, { headers: getHeaders() })
})

it('throws an error if the mockedAxios request fails', async () => {
mockedAxios.post.mockRejectedValue(new Error('Network error'))
const epic = new Epic({id: 1})
const epic = new Epic({ id: 1 })
const comment = {
text: 'Test comment',
authorId: '123',
Expand Down
Loading