diff --git a/bun.lockb b/bun.lockb index 0035d41..f1c2c63 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a585e5c..3aa3a00 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,8 @@ "./dist" ], "scripts": { - "build": "bun run build:library && bun run build:library:minified", - "build:library": "bun bunchee --runtime=browser --target=es2021", - "build:library:minified": "bun bunchee --runtime=browser --target=es2021 --minify --no-clean --no-dts --output=./dist/index.min.js", + "build": "bun run build:library", + "build:library": "bun bunchee --runtime=browser --target=es2021 --minify --output=./dist/index.mjs", "fix": "bun run fix:biome && bun run fix:package", "fix:biome": "bun biome check --write", "fix:package": "bun sort-package-json --quiet", @@ -28,13 +27,13 @@ "test": "bun run lint && bun test" }, "devDependencies": { - "@biomejs/biome": "~1.9.2", - "@types/bun": "~1.1.10", - "bunchee": "~5.4.0", - "lefthook": "~1.7.16", + "@biomejs/biome": "~1.9.4", + "@types/bun": "~1.1.13", + "bunchee": "~5.6.1", + "deepmerge-ts": "~7.1.3", + "lefthook": "~1.8.2", "sort-package-json": "~2.10.1", - "ts-deepmerge": "~7.0.1", - "typescript": "~5.6.2" + "typescript": "~5.6.3" }, "engines": { "node": ">=18.17.0" diff --git a/src/HTTP.ts b/src/HTTP.ts index 88ff43f..0206e0a 100644 --- a/src/HTTP.ts +++ b/src/HTTP.ts @@ -1,4 +1,4 @@ -import { merge } from 'ts-deepmerge'; +import { deepmerge } from 'deepmerge-ts'; import type { ClientOptions } from './types/JSP.ts'; export class HTTP { @@ -9,20 +9,26 @@ export class HTTP { } public async fetch(endpoint: string, options: RequestInit): Promise { - const requestOptions = merge(this.options.request, options) as RequestInit; + const requestOptions = deepmerge(this.options.request, options) as RequestInit; const response = await fetch(this.options.api + endpoint, requestOptions); return this.parseResponse(response); } - private parseResponse(response: Response) { + private async parseResponse(response: Response) { const contentType = response.headers.get('Content-Type'); - if (contentType?.startsWith('application/json')) { - return response.json() as Promise; + if (!contentType?.startsWith('application/json')) { + throw new Error('Unknown response type'); } - throw new Error('Unknown response type'); + if (!response.ok) { + const error = (await response.json()) as { code: number; type: string; message: string }; + + throw new Error(`${error.type}: ${error.code}: ${error.message}`); + } + + return (await response.json()) as Promise; } } diff --git a/src/JSP.spec.ts b/src/JSP.spec.ts index 88f25a5..4a7b10a 100644 --- a/src/JSP.spec.ts +++ b/src/JSP.spec.ts @@ -1,83 +1,97 @@ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import { JSP } from './JSP.ts'; -describe('V2', () => { - const jsp = new JSP(); - - const commonData = { - hello: 'Hello, World!', - bye: 'Bye, World!', - object: { - sample: 'Hello, World!' - }, - binary: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, 10]) - }; - - /** - * An unique key/secret/password for tests. - */ - const commonPrivate: string = Date.now().toString(); - const commonPrivateInvalid: string = '_:_:wrongdingdong:_:_'; - - const testCleanup = (key: string, secret: string) => { - jsp.remove(key, secret); - }; - - describe('publish', () => { - test('data only', async () => { - const response = await jsp.publish(commonData.hello); - const result = await jsp.access(response.key); - - expect(result.data).toBeDefined(); - expect(result.data).toBe(commonData.hello); - - testCleanup(response.key, response.secret); +const jsp = new JSP(); + +const commonData = { + hello: 'Hello, World!', + bye: 'Bye, World!', + object: { + sample: 'Hello, World!' + }, + binary: new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, 10]) +}; + +/** + * An unique key/secret/password for tests. + */ +const commonPrivate: string = Date.now().toString(); +const commonPrivateInvalid: string = '_:_:wrongdingdong:_:_'; + +const testCleanup = (key: string, secret: string) => { + jsp.remove(key, secret).catch(() => console.error(`Failed to cleanup "${key}" with secret "${secret}"`)); +}; + +describe('publish', async () => { + test('should response parameters be defined and valid', async () => { + // Server should prefer "key" over "keyLength" + const response = await jsp.publish(commonData.hello, { + password: commonPrivate, + key: commonPrivate, + keyLength: 20, + secret: commonPrivate }); - test('key', async () => { - const response = await jsp.publish(commonData.hello, { - key: commonPrivate - }); + expect(response.key).toBeDefined(); + expect(response.key).toBe(commonPrivate); + expect(response.secret).toBeDefined(); + expect(response.secret).toBe(commonPrivate); + expect(response.url).toBeDefined(); - expect(response.key).toBeDefined(); - expect(response.key).toBe(commonPrivate); + testCleanup(response.key, response.secret); + }); - testCleanup(response.key, response.secret); + test('should response "key" length be the same as "keyLength"', async () => { + const keyLength = 20; + const response = await jsp.publish(commonData.hello, { + keyLength }); - test.todo('keyLength', async () => { - const response = await jsp.publish(commonData.hello, { - keyLength: 20 - }); + expect(response.key).toBeDefined(); + expect(response.key.length).toBe(keyLength); + expect(response.secret).toBeDefined(); + expect(response.url).toBeDefined(); + + testCleanup(response.key, response.secret); + }); +}); - expect(response.key).toBeDefined(); - expect(response.key).toHaveLength(20); +describe('access', async () => { + const document = await jsp.publish(commonData.hello, { + secret: commonPrivate + }); - testCleanup(response.key, response.secret); - }); + const documentProtected = await jsp.publish(commonData.hello, { + password: commonPrivate, + secret: commonPrivate + }); + + afterAll(() => { + testCleanup(document.key, commonPrivate); + testCleanup(documentProtected.key, commonPrivate); + }); - test('password/secret', async () => { - const response = await jsp.publish(commonData.hello, { - password: commonPrivate, - secret: commonPrivate - }); + test('should response parameters be defined and valid', async () => { + const response = await jsp.access(document.key); - expect(response.secret).toBe(commonPrivate); + expect(response.key).toBeDefined(); + expect(response.key).toBe(document.key); + expect(response.data).toBeDefined(); + expect(response.data).toBe(commonData.hello); + expect(response.url).toBeDefined(); - const fail = await jsp.access(response.key, { - password: commonPrivateInvalid - }); + testCleanup(response.key, commonPrivate); + }); - expect(fail.data).toBeUndefined(); + test('should fail on protected document', async () => { + const responsePromise = jsp.access(documentProtected.key); - const result = await jsp.access(response.key, { - password: commonPrivate - }); + expect(responsePromise).rejects.toThrowError(); + }); - expect(result.data).toBeDefined(); - expect(result.data).toBe(commonData.hello); + test('should fail on bad password protected document', async () => { + const responsePromise = jsp.access(documentProtected.key, { password: commonPrivateInvalid }); - testCleanup(response.key, response.secret); - }); + expect(responsePromise).rejects.toThrowError(); }); }); diff --git a/src/JSP.ts b/src/JSP.ts index 334d168..7c53c4b 100644 --- a/src/JSP.ts +++ b/src/JSP.ts @@ -1,4 +1,4 @@ -import { merge } from 'ts-deepmerge'; +import { deepmerge } from 'deepmerge-ts'; import { version as libraryVersion } from '../package.json'; import { HTTP } from './HTTP.ts'; import { access } from './endpoints/v2/access.ts'; @@ -6,27 +6,25 @@ import { edit } from './endpoints/v2/edit.ts'; import { publish } from './endpoints/v2/publish.ts'; import { remove } from './endpoints/v2/remove.ts'; import type { ClientOptions } from './types/JSP.ts'; -import type { AccessOptionsV2 } from './types/endpoints/access.ts'; -import type { EditOptionsV2 } from './types/endpoints/edit.ts'; -import type { PublishOptionsV2 } from './types/endpoints/publish.ts'; +import type { AccessOptions } from './types/endpoints/access.ts'; +import type { EditOptions } from './types/endpoints/edit.ts'; +import type { PublishOptions } from './types/endpoints/publish.ts'; export class JSP { - private static readonly defaultRequestOptions: RequestInit = { - headers: { - 'User-Agent': `JSPasteHeadless/${libraryVersion} (https://github.com/jspaste/library)` - } - }; - private static readonly defaultOptions: ClientOptions = { - api: 'https://api.inetol.net/jspaste', - request: JSP.defaultRequestOptions + api: 'https://paste.inetol.net/api', + request: { + headers: { + 'User-Agent': `JSPasteHeadless/${libraryVersion} (https://github.com/jspaste/library)` + } + } }; private readonly http: HTTP; public constructor(clientOptions?: Partial) { const options = clientOptions - ? (merge(JSP.defaultOptions, clientOptions) as ClientOptions) + ? (deepmerge(JSP.defaultOptions, clientOptions) as ClientOptions) : JSP.defaultOptions; this.http = new HTTP(options); @@ -35,21 +33,21 @@ export class JSP { /** * @version API V2 */ - public async access(key: string, options?: AccessOptionsV2) { + public async access(key: string, options?: AccessOptions) { return access(this.http, key, options); } /** * @version API V2 */ - public async publish(data: string, options?: PublishOptionsV2) { + public async publish(data: string, options?: PublishOptions) { return publish(this.http, data, options); } /** * @version API V2 */ - public async edit(data: string, name: string, secret: string, options?: EditOptionsV2) { + public async edit(data: string, name: string, secret: string, options?: EditOptions) { return edit(this.http, data, name, secret, options); } diff --git a/src/endpoints/v2/access.ts b/src/endpoints/v2/access.ts index 2e32902..f8088bd 100644 --- a/src/endpoints/v2/access.ts +++ b/src/endpoints/v2/access.ts @@ -1,8 +1,8 @@ import type { HTTP } from '../../HTTP.ts'; -import type { AccessOptionsV2, AccessResponseV2 } from '../../types/endpoints/access.ts'; +import type { AccessOptions, AccessResponse } from '../../types/endpoints/access.ts'; -export const access = async (http: HTTP, name: string, options?: AccessOptionsV2) => { - return http.fetch(`/v2/documents/${name}`, { +export const access = async (http: HTTP, name: string, options?: AccessOptions) => { + return http.fetch(`/v2/documents/${name}`, { method: 'GET', headers: { ...(options?.password && { password: options.password }) diff --git a/src/endpoints/v2/edit.ts b/src/endpoints/v2/edit.ts index e44951a..944cd18 100644 --- a/src/endpoints/v2/edit.ts +++ b/src/endpoints/v2/edit.ts @@ -1,8 +1,8 @@ import type { HTTP } from '../../HTTP.ts'; -import type { EditOptionsV2, EditResponseV2 } from '../../types/endpoints/edit.ts'; +import type { EditOptions, EditResponse } from '../../types/endpoints/edit.ts'; -export const edit = async (http: HTTP, data: string, name: string, secret: string, options?: EditOptionsV2) => { - return http.fetch(`/v2/documents/${name}`, { +export const edit = async (http: HTTP, data: string, name: string, secret: string, options?: EditOptions) => { + return http.fetch(`/v2/documents/${name}`, { method: 'PATCH', body: data, headers: { diff --git a/src/endpoints/v2/publish.ts b/src/endpoints/v2/publish.ts index 365a67d..6a8e4f0 100644 --- a/src/endpoints/v2/publish.ts +++ b/src/endpoints/v2/publish.ts @@ -1,8 +1,8 @@ import type { HTTP } from '../../HTTP.ts'; -import type { PublishOptionsV2, PublishResponseV2 } from '../../types/endpoints/publish.ts'; +import type { PublishOptions, PublishResponse } from '../../types/endpoints/publish.ts'; -export const publish = async (http: HTTP, data: string, options?: PublishOptionsV2) => { - return http.fetch('/v2/documents', { +export const publish = async (http: HTTP, data: string, options?: PublishOptions) => { + return http.fetch('/v2/documents', { method: 'POST', body: data, headers: { diff --git a/src/endpoints/v2/remove.ts b/src/endpoints/v2/remove.ts index b2ceb52..813c9f3 100644 --- a/src/endpoints/v2/remove.ts +++ b/src/endpoints/v2/remove.ts @@ -1,8 +1,8 @@ import type { HTTP } from '../../HTTP.ts'; -import type { RemoveResponseV2 } from '../../types/endpoints/remove.ts'; +import type { RemoveResponse } from '../../types/endpoints/remove.ts'; export const remove = async (http: HTTP, name: string, secret: string) => { - return http.fetch(`/v2/documents/${name}`, { + return http.fetch(`/v2/documents/${name}`, { method: 'DELETE', headers: { secret: secret diff --git a/src/types/endpoints/access.ts b/src/types/endpoints/access.ts index 425cf30..795f8ee 100644 --- a/src/types/endpoints/access.ts +++ b/src/types/endpoints/access.ts @@ -1,12 +1,11 @@ -type AccessOptionsV2 = { +type AccessOptions = { password?: string; }; -type AccessResponseV2 = { +type AccessResponse = { key: string; data: string; url: string; - expirationTimestamp: number; }; -export type { AccessOptionsV2, AccessResponseV2 }; +export type { AccessOptions, AccessResponse }; diff --git a/src/types/endpoints/edit.ts b/src/types/endpoints/edit.ts index 1f8eca7..8a3c3c3 100644 --- a/src/types/endpoints/edit.ts +++ b/src/types/endpoints/edit.ts @@ -1,9 +1,9 @@ -type EditOptionsV2 = { +type EditOptions = { password?: string; }; -type EditResponseV2 = { +type EditResponse = { edited: boolean; }; -export type { EditOptionsV2, EditResponseV2 }; +export type { EditOptions, EditResponse }; diff --git a/src/types/endpoints/publish.ts b/src/types/endpoints/publish.ts index a976fda..0097bc5 100644 --- a/src/types/endpoints/publish.ts +++ b/src/types/endpoints/publish.ts @@ -1,15 +1,14 @@ -type PublishOptionsV2 = { +type PublishOptions = { password?: string; key?: string; keyLength?: number; secret?: string; }; -type PublishResponseV2 = { +type PublishResponse = { key: string; secret: string; url: string; - expirationTimestamp: number; }; -export type { PublishOptionsV2, PublishResponseV2 }; +export type { PublishOptions, PublishResponse }; diff --git a/src/types/endpoints/remove.ts b/src/types/endpoints/remove.ts index c899e2c..c31f390 100644 --- a/src/types/endpoints/remove.ts +++ b/src/types/endpoints/remove.ts @@ -1,5 +1,5 @@ -type RemoveResponseV2 = { +type RemoveResponse = { removed: boolean; }; -export type { RemoveResponseV2 }; +export type { RemoveResponse };