diff --git a/nodejs/README.md b/nodejs/README.md index 56b7d94..2e93e1b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -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 +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. @@ -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 diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 3389ee4..1ea9bb7 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -26,6 +26,8 @@ import { UpdateFolderOrderBody, UpdateTeamFolderBody, UpdateUserFolderBody, + UploadNoteImageOptions, + UploadNoteImageResponse, } from './type' import * as HackMDErrors from './error' @@ -70,9 +72,6 @@ export class API { this.axios = axios.create({ baseURL: hackmdAPIEndpointURL, - headers:{ - "Content-Type": "application/json", - }, timeout: options.timeout }) @@ -209,6 +208,16 @@ export class API { return this.unwrapData(this.axios.delete(`notes/${noteId}`), options.unwrapData) as unknown as OptionReturnType } + async uploadNoteImage (noteId: string, image: Blob, options = defaultOption as Opt): Promise> { + const formData = new FormData() + formData.append('image', image, options.filename ?? undefined) + + return this.unwrapData( + this.axios.post(`notes/${noteId}/images`, formData), + options.unwrapData, + ) as unknown as OptionReturnType + } + async getTeams (options = defaultOption as Opt): Promise> { return this.unwrapData(this.axios.get("teams"), options.unwrapData) as unknown as OptionReturnType } diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 50eb3f3..9dd93e8 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -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[] diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 9f3f28f..998c8c0 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -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 diff --git a/nodejs/tests/e2e/api.e2e.spec.ts b/nodejs/tests/e2e/api.e2e.spec.ts index 8729e6b..8a350fb 100644 --- a/nodejs/tests/e2e/api.e2e.spec.ts +++ b/nodejs/tests/e2e/api.e2e.spec.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'fs' import type { ApiFolderOrder } from '../../src' import { API } from '../../src' import { HttpResponseError } from '../../src/error' @@ -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) diff --git a/nodejs/tests/fixtures/hackmd-cute-logo.png b/nodejs/tests/fixtures/hackmd-cute-logo.png new file mode 100644 index 0000000..8d20cd0 Binary files /dev/null and b/nodejs/tests/fixtures/hackmd-cute-logo.png differ