Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 18 additions & 1 deletion nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ const updatedNote = await client.getNote('note-id', { etag })
// If the note hasn't changed, the response will have status 304
```

### Image Upload

Upload an image to a note with `uploadNoteImage`. The API returns the uploaded image link in `data.link`.

```javascript
// Browser: pass a File from an <input type="file">
const uploaded = await client.uploadNoteImage('note-id', file)
console.log(uploaded.data.link)

// Node.js 18+: pass a Blob and optional filename
const image = new Blob([imageBuffer], { type: 'image/png' })
const uploadedFromNode = await client.uploadNoteImage('note-id', image, {
filename: 'diagram.png'
})
console.log(uploadedFromNode.data.link)
```

## API

See the [code](./src/index.ts) and [typings](./src/type.ts). The API client is written in TypeScript, so you can get auto-completion and type checking in any TypeScript Language Server powered editor or IDE.
Expand All @@ -143,7 +160,7 @@ npm run test:e2e

Set `HACKMD_E2E_MUTATIONS=1` to run write tests against your account:

- **Notes:** create → get → update (title, content, tags) → list → delete.
- **Notes:** create → get → update (title, content, tags) → upload fixture image → list → delete.
- **Folders:** one integration test runs create (root + nested) → get → update → list → folder-order round-trip (skipped if that API returns 404) → delete. If **POST `/folders`** returns 404 (common before full production rollout), the test exits early with a warning; use staging or `HACKMD_E2E_FOLDERS=0`.

```bash
Expand Down
15 changes: 12 additions & 3 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
UpdateFolderOrderBody,
UpdateTeamFolderBody,
UpdateUserFolderBody,
UploadNoteImageOptions,
UploadNoteImageResponse,
} from './type'
import * as HackMDErrors from './error'

Expand Down Expand Up @@ -70,9 +72,6 @@ export class API {

this.axios = axios.create({
baseURL: hackmdAPIEndpointURL,
headers:{
"Content-Type": "application/json",
},
timeout: options.timeout
})

Expand Down Expand Up @@ -209,6 +208,16 @@ export class API {
return this.unwrapData(this.axios.delete<SingleNote>(`notes/${noteId}`), options.unwrapData) as unknown as OptionReturnType<Opt, SingleNote>
}

async uploadNoteImage<Opt extends UploadNoteImageOptions> (noteId: string, image: Blob, options = defaultOption as Opt): Promise<OptionReturnType<Opt, UploadNoteImageResponse>> {
const formData = new FormData()
formData.append('image', image, options.filename ?? undefined)

return this.unwrapData(
this.axios.post<UploadNoteImageResponse>(`notes/${noteId}/images`, formData),
options.unwrapData,
) as unknown as OptionReturnType<Opt, UploadNoteImageResponse>
}

async getTeams<Opt extends RequestOptions> (options = defaultOption as Opt): Promise<OptionReturnType<Opt, GetUserTeams>> {
return this.unwrapData(this.axios.get<GetUserTeams>("teams"), options.unwrapData) as unknown as OptionReturnType<Opt, GetUserTeams>
}
Expand Down
11 changes: 11 additions & 0 deletions nodejs/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ export type CreateUserNote = SingleNote
export type UpdateUserNote = void
export type DeleteUserNote = void

export type UploadNoteImageResponse = {
data: {
link: string
}
}

export type UploadNoteImageOptions = {
unwrapData?: boolean
filename?: string
}

// Teams
export type GetUserTeams = Team[]

Expand Down
35 changes: 35 additions & 0 deletions nodejs/tests/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,41 @@ test('updateFolderOrder sends order payload', async () => {
})
})

test('uploadNoteImage sends image as multipart form data', async () => {
let uploaded: FormDataEntryValue | null = null
let contentType: string | null = null

server.use(
http.post('https://api.hackmd.io/v1/notes/test-note-id/images', async ({ request }) => {
contentType = request.headers.get('content-type')
const formData = await request.formData()
uploaded = formData.get('image')

return HttpResponse.json({
data: {
link: 'https://hackmd.io/_uploads/test-image.png',
},
})
}),
)

const response = await client.uploadNoteImage(
'test-note-id',
new Blob(['test image'], { type: 'image/png' }),
{ filename: 'test-image.png' },
)

expect(contentType).toContain('multipart/form-data')
expect(uploaded).toBeInstanceOf(Blob)
const uploadedBlob = uploaded as unknown as Blob
expect(uploadedBlob.type).toBe('image/png')
expect(response).toEqual({
data: {
link: 'https://hackmd.io/_uploads/test-image.png',
},
})
})

test('should support updating team note title and tags metadata', async () => {
const updatedTags = ['team', 'metadata']
let requestBody: unknown
Expand Down
14 changes: 14 additions & 0 deletions nodejs/tests/e2e/api.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readFileSync } from 'fs'
import type { ApiFolderOrder } from '../../src'
import { API } from '../../src'
import { HttpResponseError } from '../../src/error'
Expand Down Expand Up @@ -144,6 +145,19 @@ describe('HackMD API (live e2e)', () => {
}
})

it('uploadNoteImage uploads an image to the note', async () => {
const image = readFileSync('tests/fixtures/hackmd-cute-logo.png')

const uploaded = await client.uploadNoteImage(
noteId,
new Blob([new Uint8Array(image)], { type: 'image/png' }),
{ filename: `hackmd-cute-logo-${stamp}.png` },
)

expect(uploaded.data.link).toEqual(expect.any(String))
expect(uploaded.data.link.length).toBeGreaterThan(0)
})

it('getNoteList includes the note', async () => {
const list = await client.getNoteList()
const found = list.find(n => n.id === noteId)
Expand Down
Binary file added nodejs/tests/fixtures/hackmd-cute-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading