Skip to content
This repository has been archived by the owner on Jan 25, 2025. It is now read-only.

Commit

Permalink
v1.0.0 (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
JensAstrup authored Apr 9, 2024
2 parents 3d0ffc5 + bbdf98a commit e0f4342
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 65 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"error",
"never"
],
"brace-style": ["error", "stroustrup"],
"perfectionist/sort-imports": "off",
"perfectionist/sort-union-types": "off",
"perfectionist/sort-classes": "off",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shortcut-api",
"version": "1.0.0-beta.19",
"version": "1.0.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion src/base-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import UUID from '@sx/utils/uuid'
* Base interface for Shortcut resources, representing the data returned from the API following JavaScript naming conventions.
*/
export default interface BaseInterface {
id: UUID | number
id: UUID | number | null
}
5 changes: 4 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import UploadedFilesService from '@sx/uploaded-files/uploaded-files-service'
import * as process from 'process'

import CustomFieldsService from '@sx/custom-fields/custom-fields-service'
Expand All @@ -9,8 +8,10 @@ import LabelsService from '@sx/labels/labels-service'
import LinkedFilesService from '@sx/linked-files/linked-files-service'
import MembersService from '@sx/members/members-service'
import ObjectivesService from '@sx/objectives/objectives-service'
import RepositoriesService from '@sx/repositories/repositories-service'
import StoriesService from '@sx/stories/stories-service'
import TeamsService from '@sx/teams/teams-service'
import UploadedFilesService from '@sx/uploaded-files/uploaded-files-service'
import WorkflowService from '@sx/workflows/workflows-service'


Expand Down Expand Up @@ -46,6 +47,7 @@ export default class Client {
public customFields: CustomFieldsService
public linkedFiles: LinkedFilesService
public uploadedFiles: UploadedFilesService
public repositories: RepositoriesService

constructor(shortcutApiKey?: string) {
if (shortcutApiKey) this.shortcutApiKey = shortcutApiKey
Expand All @@ -61,6 +63,7 @@ export default class Client {
this.customFields = new CustomFieldsService({headers: this.headers})
this.linkedFiles = new LinkedFilesService({headers: this.headers})
this.uploadedFiles = new UploadedFilesService({headers: this.headers})
this.repositories = new RepositoriesService({headers: this.headers})
}

}
23 changes: 23 additions & 0 deletions src/repositories/contracts/repository-api-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import BaseData from '@sx/base-data'


enum RepositoryType {
BITBUCKET = 'bitbucket',
GITHUB = 'github',
GITLAB = 'gitlab'
}

interface RepositoryApiData extends BaseData {
created_at: string | null
entity_type: string | null
external_id: string | null
full_name: string | null
id: number | null
name: string | null
type: RepositoryType | null
updated_at: string | null
url: string | null
}

export default RepositoryApiData
export { RepositoryType }
17 changes: 17 additions & 0 deletions src/repositories/contracts/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BaseInterface from '@sx/base-interface'
import {RepositoryType} from '@sx/repositories/contracts/repository-api-data'


interface RepositoryInterface extends BaseInterface {
createdAt: Date | null
entityType: string | null
externalId: string | null
fullName: string | null
id: number | null
name: string | null
type: RepositoryType
updatedAt: Date | null
url: string | null
}

export default RepositoryInterface
12 changes: 12 additions & 0 deletions src/repositories/repositories-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BaseService, {ServiceOperation} from '@sx/base-service'
import RepositoryInterface from '@sx/repositories/contracts/repository'
import Repository from '@sx/repositories/repository'


class RepositoriesService extends BaseService<Repository, RepositoryInterface>{
public baseUrl = 'https://api.app.shortcut.com/api/v3/repositories'
protected factory = (data: RepositoryInterface) => new Repository(data)
public availableOperations: ServiceOperation[] = ['get', 'list']
}

export default RepositoriesService
26 changes: 26 additions & 0 deletions src/repositories/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ShortcutResource, {ResourceOperation} from '@sx/base-resource'
import RepositoryInterface from '@sx/repositories/contracts/repository'
import {RepositoryType} from '@sx/repositories/contracts/repository-api-data'


class Repository extends ShortcutResource<RepositoryInterface> implements RepositoryInterface {
public availableOperations: ResourceOperation[] = []

createdAt: Date | null
entityType: string | null
externalId: string | null
fullName: string | null
id: number | null
name: string | null
type: RepositoryType
updatedAt: Date | null
url: string | null

constructor(init: RepositoryInterface) {
super()
Object.assign(this, init)
this.changedFields = []
}
}

export default Repository
16 changes: 16 additions & 0 deletions src/stories/stories-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import axios from 'axios'

import {BaseSearchableService, ServiceOperation} from '@sx/base-service'
import {StoryApiData} from '@sx/stories/contracts/story-api-data'
import StoryInterface from '@sx/stories/contracts/story-interface'
import Story from '@sx/stories/story'
import {convertApiFields} from '@sx/utils/convert-fields'


/**
Expand All @@ -14,4 +18,16 @@ export default class StoriesService extends BaseSearchableService<Story, StoryIn
constructor(init: { headers: Record<string, string> }) {
super(init)
}

/**
* Fetches all stories that have an external link associated with them.
*/
async getExternallyLinked(link: string): Promise<Story[]> {
const url = 'https://api.app.shortcut.com/api/v3/external-link/stories'
const response = await axios.get(url, { headers: this.headers, params: { external_link: link } })
return response.data.map((data: StoryApiData) => {
const interfaceData = convertApiFields(data)
return this.factory(interfaceData)
})
}
}
9 changes: 6 additions & 3 deletions src/stories/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ class Story extends ShortcutResource<StoryInterface> implements StoryInterface {
throw new Error(`Error creating comment: ${error}`)
})
const data: StoryCommentApiData = response.data
return convertApiFields(data) as StoryComment
const interfaceData = convertApiFields(data)

return new StoryComment(interfaceData)
}

public async addFile(file: Buffer): Promise<UploadedFile> {
Expand All @@ -195,7 +197,8 @@ class Story extends ShortcutResource<StoryInterface> implements StoryInterface {
throw new Error(`Error adding task: ${error}`)
})
const data: TaskApiData = response.data
const createdTask = convertApiFields(data) as Task
const interfaceData = convertApiFields(data)
const createdTask = new Task(interfaceData)
this.tasks.push(createdTask)
}

Expand Down Expand Up @@ -286,4 +289,4 @@ class Story extends ShortcutResource<StoryInterface> implements StoryInterface {
pullRequests: PullRequestInterface[]
}

export default Story
export default Story
16 changes: 10 additions & 6 deletions src/utils/convert-fields.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
import BaseCreateInterface from '@sx/base-create-interface'
import BaseData from '@sx/base-data'
import BaseInterface from '@sx/base-interface'
import ShortcutResource from '@sx/base-resource'
import camelToSnake from '@sx/utils/camel-to-snake'
import isValidDatetimeFormat from '@sx/utils/is-valid-datetime-format'
import snakeToCamel from '@sx/utils/snake-to-camel'


type AnyObject = Record<string, unknown>

export function convertApiFields<Input extends BaseData, Resource extends ShortcutResource | BaseInterface>(object: Input): Resource {
function convertApiFields<Input extends BaseData, Resource extends BaseInterface>(object: Input): Resource {
const convertObject = (obj: BaseData | BaseData[]): AnyObject | Array<object> => {
if (Array.isArray(obj)) {
return obj.map(item => convertObject(item)) // Recursively process each item in the array
} else if (obj !== null && typeof obj === 'object') {
}
else if (obj !== null && typeof obj === 'object') {
const newObj: AnyObject = {}
Object.keys(obj).forEach(key => {
const camelKey = snakeToCamel(key)
const value = obj[key]
if (isValidDatetimeFormat(value as string)) {

newObj[camelKey] = new Date(value as string)
} else {
}
else {
newObj[camelKey] = typeof obj[key] === 'object' ? convertObject(obj[key] as AnyObject) : obj[key]
}
})
return newObj
} else {
}
else {
return obj
}
}

return convertObject(object) as Resource
}

export function convertToApiFields<Input extends BaseCreateInterface, U extends BaseData>(object: Input): U {
function convertToApiFields<Input extends BaseCreateInterface, U extends BaseData>(object: Input): U {
const convertObject = (obj: BaseCreateInterface | BaseCreateInterface[]): AnyObject | Array<object> => {
if (Array.isArray(obj)) {
return obj.map(item => convertObject(item)) // Recursively process each item in the array
Expand All @@ -60,3 +62,5 @@ export function convertToApiFields<Input extends BaseCreateInterface, U extends

return convertObject(object) as U
}

export {convertApiFields, convertToApiFields}
11 changes: 11 additions & 0 deletions tests/respositories/repositories-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import RepositoriesService from '@sx/repositories/repositories-service'


describe('RepositoriesService', () => {
it('should instantiate a new RepositoriesService', () => {
const repositoriesService = new RepositoriesService({headers: {}})
expect(repositoriesService).toBeInstanceOf(RepositoriesService)
expect(repositoriesService.baseUrl).toEqual('https://api.app.shortcut.com/api/v3/repositories')
expect(repositoriesService.availableOperations).toEqual(['get', 'list'])
})
})
30 changes: 30 additions & 0 deletions tests/respositories/repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import RepositoryInterface from '@sx/repositories/contracts/repository'
import Repository from '@sx/repositories/repository'


describe('Repository', () => {
it('should instantiate a new Repository', () => {
const repositoryData = {
createdAt: new Date(),
entityType: 'entityType',
externalId: 'externalId',
fullName: 'fullName',
id: 1,
name: 'name',
type: 'type',
updatedAt: new Date(),
url: 'url'
} as object as RepositoryInterface
const repository = new Repository(repositoryData)
expect(repository).toBeInstanceOf(Repository)
expect(repository.createdAt).toEqual(repositoryData.createdAt)
expect(repository.entityType).toEqual(repositoryData.entityType)
expect(repository.externalId).toEqual(repositoryData.externalId)
expect(repository.fullName).toEqual(repositoryData.fullName)
expect(repository.id).toEqual(repositoryData.id)
expect(repository.name).toEqual(repositoryData.name)
expect(repository.type).toEqual(repositoryData.type)
expect(repository.updatedAt).toEqual(repositoryData.updatedAt)
expect(repository.url).toEqual(repositoryData.url)
})
})
49 changes: 0 additions & 49 deletions tests/stories/stories-service.test.js

This file was deleted.

Loading

0 comments on commit e0f4342

Please sign in to comment.