Skip to content

Commit 6029ff1

Browse files
authored
feat: support upload note image api (#55)
2 parents 99a5117 + d101e4c commit 6029ff1

6 files changed

Lines changed: 90 additions & 4 deletions

File tree

nodejs/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ const updatedNote = await client.getNote('note-id', { etag })
117117
// If the note hasn't changed, the response will have status 304
118118
```
119119

120+
### Image Upload
121+
122+
Upload an image to a note with `uploadNoteImage`. The API returns the uploaded image link in `data.link`.
123+
124+
```javascript
125+
// Browser: pass a File from an <input type="file">
126+
const uploaded = await client.uploadNoteImage('note-id', file)
127+
console.log(uploaded.data.link)
128+
129+
// Node.js 18+: pass a Blob and optional filename
130+
const image = new Blob([imageBuffer], { type: 'image/png' })
131+
const uploadedFromNode = await client.uploadNoteImage('note-id', image, {
132+
filename: 'diagram.png'
133+
})
134+
console.log(uploadedFromNode.data.link)
135+
```
136+
120137
## API
121138

122139
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.
@@ -143,7 +160,7 @@ npm run test:e2e
143160

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

146-
- **Notes:** create → get → update (title, content, tags) → list → delete.
163+
- **Notes:** create → get → update (title, content, tags) → upload fixture image → list → delete.
147164
- **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`.
148165

149166
```bash

nodejs/src/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
UpdateFolderOrderBody,
2727
UpdateTeamFolderBody,
2828
UpdateUserFolderBody,
29+
UploadNoteImageOptions,
30+
UploadNoteImageResponse,
2931
} from './type'
3032
import * as HackMDErrors from './error'
3133

@@ -70,9 +72,6 @@ export class API {
7072

7173
this.axios = axios.create({
7274
baseURL: hackmdAPIEndpointURL,
73-
headers:{
74-
"Content-Type": "application/json",
75-
},
7675
timeout: options.timeout
7776
})
7877

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

211+
async uploadNoteImage<Opt extends UploadNoteImageOptions> (noteId: string, image: Blob, options = defaultOption as Opt): Promise<OptionReturnType<Opt, UploadNoteImageResponse>> {
212+
const formData = new FormData()
213+
formData.append('image', image, options.filename ?? undefined)
214+
215+
return this.unwrapData(
216+
this.axios.post<UploadNoteImageResponse>(`notes/${noteId}/images`, formData),
217+
options.unwrapData,
218+
) as unknown as OptionReturnType<Opt, UploadNoteImageResponse>
219+
}
220+
212221
async getTeams<Opt extends RequestOptions> (options = defaultOption as Opt): Promise<OptionReturnType<Opt, GetUserTeams>> {
213222
return this.unwrapData(this.axios.get<GetUserTeams>("teams"), options.unwrapData) as unknown as OptionReturnType<Opt, GetUserTeams>
214223
}

nodejs/src/type.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ export type CreateUserNote = SingleNote
114114
export type UpdateUserNote = void
115115
export type DeleteUserNote = void
116116

117+
export type UploadNoteImageResponse = {
118+
data: {
119+
link: string
120+
}
121+
}
122+
123+
export type UploadNoteImageOptions = {
124+
unwrapData?: boolean
125+
filename?: string
126+
}
127+
117128
// Teams
118129
export type GetUserTeams = Team[]
119130

nodejs/tests/api.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,41 @@ test('updateFolderOrder sends order payload', async () => {
143143
})
144144
})
145145

146+
test('uploadNoteImage sends image as multipart form data', async () => {
147+
let uploaded: FormDataEntryValue | null = null
148+
let contentType: string | null = null
149+
150+
server.use(
151+
http.post('https://api.hackmd.io/v1/notes/test-note-id/images', async ({ request }) => {
152+
contentType = request.headers.get('content-type')
153+
const formData = await request.formData()
154+
uploaded = formData.get('image')
155+
156+
return HttpResponse.json({
157+
data: {
158+
link: 'https://hackmd.io/_uploads/test-image.png',
159+
},
160+
})
161+
}),
162+
)
163+
164+
const response = await client.uploadNoteImage(
165+
'test-note-id',
166+
new Blob(['test image'], { type: 'image/png' }),
167+
{ filename: 'test-image.png' },
168+
)
169+
170+
expect(contentType).toContain('multipart/form-data')
171+
expect(uploaded).toBeInstanceOf(Blob)
172+
const uploadedBlob = uploaded as unknown as Blob
173+
expect(uploadedBlob.type).toBe('image/png')
174+
expect(response).toEqual({
175+
data: {
176+
link: 'https://hackmd.io/_uploads/test-image.png',
177+
},
178+
})
179+
})
180+
146181
test('should support updating team note title and tags metadata', async () => {
147182
const updatedTags = ['team', 'metadata']
148183
let requestBody: unknown

nodejs/tests/e2e/api.e2e.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readFileSync } from 'fs'
12
import type { ApiFolderOrder } from '../../src'
23
import { API } from '../../src'
34
import { HttpResponseError } from '../../src/error'
@@ -144,6 +145,19 @@ describe('HackMD API (live e2e)', () => {
144145
}
145146
})
146147

148+
it('uploadNoteImage uploads an image to the note', async () => {
149+
const image = readFileSync('tests/fixtures/hackmd-cute-logo.png')
150+
151+
const uploaded = await client.uploadNoteImage(
152+
noteId,
153+
new Blob([new Uint8Array(image)], { type: 'image/png' }),
154+
{ filename: `hackmd-cute-logo-${stamp}.png` },
155+
)
156+
157+
expect(uploaded.data.link).toEqual(expect.any(String))
158+
expect(uploaded.data.link.length).toBeGreaterThan(0)
159+
})
160+
147161
it('getNoteList includes the note', async () => {
148162
const list = await client.getNoteList()
149163
const found = list.find(n => n.id === noteId)
65.6 KB
Loading

0 commit comments

Comments
 (0)