@generatepdfs/node-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GeneratePDFs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+
25
+
26
+
27
+
28
+
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # GeneratePDFs Node.js SDK
2
+
3
+ Node.js SDK for the [GeneratePDFs.com](https://generatepdfs.com) API, your go-to place for HTML to PDF.
4
+
5
+ Upload your HTML files, along with any CSS files and images to generate a PDF. Alternatively provide a URL to generate a PDF from it's contents.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @generatepdfs/node-sdk
11
+ ```
12
+
13
+ ## Get your API Token
14
+
15
+ Sign up for an account on [GeneratePDFs.com](https://generatepdfs.com) and head to the API Tokens section and create a new token.
16
+
17
+ ## Usage
18
+
19
+ ### Basic Setup
20
+
21
+ ```javascript
22
+ import { GeneratePDFs } from '@generatepdfs/node-sdk';
23
+
24
+ const client = GeneratePDFs.connect('YOUR_API_TOKEN');
25
+ ```
26
+
27
+ ### Generate PDF from HTML File
28
+
29
+ ```javascript
30
+ import { GeneratePDFs } from '@generatepdfs/node-sdk';
31
+
32
+ // Simple HTML file
33
+ const pdf = await client.generateFromHtml('/path/to/file.html');
34
+
35
+ // HTML file with CSS
36
+ const pdf = await client.generateFromHtml(
37
+ '/path/to/file.html',
38
+ '/path/to/file.css'
39
+ );
40
+
41
+ // HTML file with CSS and images
42
+ const pdf = await client.generateFromHtml(
43
+ '/path/to/file.html',
44
+ '/path/to/file.css',
45
+ [
46
+ {
47
+ name: 'logo.png',
48
+ path: '/path/to/logo.png',
49
+ mimeType: 'image/png' // Optional, will be auto-detected
50
+ },
51
+ {
52
+ name: 'photo.jpg',
53
+ path: '/path/to/photo.jpg'
54
+ }
55
+ ]
56
+ );
57
+ ```
58
+
59
+ ### Generate PDF from URL
60
+
61
+ ```javascript
62
+ const pdf = await client.generateFromUrl('https://example.com');
63
+ ```
64
+
65
+ ### Get PDF by ID
66
+
67
+ ```javascript
68
+ // Retrieve a PDF by its ID
69
+ const pdf = await client.getPdf(123);
70
+ ```
71
+
72
+ ### Working with PDF Objects
73
+
74
+ The SDK returns `Pdf` objects that provide easy access to PDF information and downloading:
75
+
76
+ ```javascript
77
+ // Access PDF properties
78
+ const pdfId = pdf.getId();
79
+ const pdfName = pdf.getName();
80
+ const status = pdf.getStatus();
81
+ const downloadUrl = pdf.getDownloadUrl();
82
+ const createdAt = pdf.getCreatedAt();
83
+
84
+ // Check if PDF is ready
85
+ if (pdf.isReady()) {
86
+ // Download PDF content as Buffer
87
+ const pdfContent = await pdf.download();
88
+
89
+ // Or save directly to file
90
+ await pdf.downloadToFile('/path/to/save/output.pdf');
91
+ }
92
+
93
+ // Refresh PDF data from the API (useful for checking status updates)
94
+ const refreshedPdf = await pdf.refresh();
95
+ if (refreshedPdf.isReady()) {
96
+ const pdfContent = await refreshedPdf.download();
97
+ }
98
+ ```
99
+
100
+ ### Client Methods
101
+
102
+ - `generateFromHtml(htmlPath: string, cssPath?: string | null, images?: ImageInput[]): Promise<Pdf>` - Generate a PDF from HTML file(s)
103
+ - `generateFromUrl(url: string): Promise<Pdf>` - Generate a PDF from a URL
104
+ - `getPdf(id: number): Promise<Pdf>` - Retrieve a PDF by its ID
105
+ - `downloadPdf(downloadUrl: string): Promise<Buffer>` - Download PDF binary content from a download URL
106
+
107
+ ### PDF Object Methods
108
+
109
+ - `getId(): number` - Get the PDF ID
110
+ - `getName(): string` - Get the PDF filename
111
+ - `getStatus(): string` - Get the current status (pending, processing, completed, failed)
112
+ - `getDownloadUrl(): string` - Get the download URL
113
+ - `getCreatedAt(): Date` - Get the creation date
114
+ - `isReady(): boolean` - Check if the PDF is ready for download
115
+ - `download(): Promise<Buffer>` - Download and return PDF binary content
116
+ - `downloadToFile(filePath: string): Promise<boolean>` - Download and save PDF to a file
117
+ - `refresh(): Promise<Pdf>` - Refresh PDF data from the API and return a new Pdf instance with updated information
118
+
119
+ ## Requirements
120
+
121
+ - Node.js 18.0 or higher
122
+
123
+ ## Testing
124
+
125
+ To run the test suite, execute:
126
+
127
+ ```bash
128
+ npm test
129
+ ```
130
+
131
+ To run tests with coverage:
132
+
133
+ ```bash
134
+ npm run test:coverage
135
+ ```
136
+
137
+ ## Contributing
138
+
139
+ Contributions and suggestions are **welcome** and will be fully **credited**.
140
+
141
+ We accept contributions via Pull Requests on [GitHub](https://github.com/GeneratePDFs/node-sdk).
142
+
143
+ ### Pull Requests
144
+
145
+ - **Follow JavaScript best practices** - Write clean, maintainable code
146
+ - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
147
+ - **Document any change in behaviour** - Make sure the README / CHANGELOG and any other relevant documentation are kept up-to-date.
148
+ - **Consider our release cycle** - We try to follow semver. Randomly breaking public APIs is not an option.
149
+ - **Create topic branches** - Don't ask us to pull from your master branch.
150
+ - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
151
+ - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
152
+
153
+ ## Changelog
154
+
155
+ See [CHANGELOG.md](CHANGELOG.md) for a history of changes.
156
+
157
+ ## License
158
+
159
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
160
+
161
+
162
+
163
+
164
+
165
+
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@generatepdfs/node-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Node.js SDK for GeneratePDFs.com API",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "scripts": {
11
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
12
+ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
13
+ "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
14
+ "lint": "eslint src --ext .js",
15
+ "lint:fix": "eslint src --ext .js --fix"
16
+ },
17
+ "keywords": [
18
+ "pdf",
19
+ "generate",
20
+ "api",
21
+ "sdk",
22
+ "nodejs"
23
+ ],
24
+ "author": "GeneratePDFs",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/GeneratePDFs/node-sdk"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "dependencies": {},
34
+ "devDependencies": {
35
+ "@jest/globals": "^29.7.0",
36
+ "eslint": "^8.57.0",
37
+ "jest": "^29.7.0"
38
+ }
39
+ }
40
+
@@ -0,0 +1,244 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { Pdf } from './Pdf.js';
3
+ import { InvalidArgumentException } from './exceptions/InvalidArgumentException.js';
4
+
5
+ export class GeneratePDFs {
6
+ static BASE_URL = 'https://api.generatepdfs.com';
7
+ #apiToken;
8
+ #baseUrl;
9
+
10
+ constructor(apiToken) {
11
+ this.#apiToken = apiToken;
12
+ this.#baseUrl = GeneratePDFs.BASE_URL;
13
+ }
14
+
15
+ /**
16
+ * Create a new GeneratePDFs instance with the provided API token.
17
+ *
18
+ * @param {string} apiToken The API token for authentication
19
+ * @returns {GeneratePDFs} GeneratePDFs instance
20
+ */
21
+ static connect(apiToken) {
22
+ return new GeneratePDFs(apiToken);
23
+ }
24
+
25
+ /**
26
+ * Generate a PDF from HTML file(s) with optional CSS and images.
27
+ *
28
+ * @param {string} htmlPath Path to the HTML file
29
+ * @param {string|null} cssPath Optional path to the CSS file
30
+ * @param {Array<{name: string, path: string, mimeType?: string}>} images Optional array of image files
31
+ * @returns {Promise<Pdf>} PDF object containing PDF information
32
+ * @throws {InvalidArgumentException} If files are invalid
33
+ */
34
+ async generateFromHtml(htmlPath, cssPath = null, images = []) {
35
+ if (!existsSync(htmlPath)) {
36
+ throw new InvalidArgumentException(`HTML file not found or not readable: ${htmlPath}`);
37
+ }
38
+
39
+ const htmlContent = readFileSync(htmlPath);
40
+ const htmlBase64 = htmlContent.toString('base64');
41
+
42
+ const data = {
43
+ html: htmlBase64,
44
+ };
45
+
46
+ if (cssPath !== null && cssPath !== undefined) {
47
+ if (!existsSync(cssPath)) {
48
+ throw new InvalidArgumentException(`CSS file not found or not readable: ${cssPath}`);
49
+ }
50
+
51
+ const cssContent = readFileSync(cssPath);
52
+ data.css = cssContent.toString('base64');
53
+ }
54
+
55
+ if (images.length > 0) {
56
+ data.images = this.#processImages(images);
57
+ }
58
+
59
+ const response = await this.#makeRequest('/pdfs/generate', data);
60
+
61
+ if (!response.data) {
62
+ throw new InvalidArgumentException('Invalid API response: missing data');
63
+ }
64
+
65
+ return Pdf.fromArray(response.data, this);
66
+ }
67
+
68
+ /**
69
+ * Generate a PDF from a URL.
70
+ *
71
+ * @param {string} url The URL to convert to PDF
72
+ * @returns {Promise<Pdf>} PDF object containing PDF information
73
+ * @throws {InvalidArgumentException} If URL is invalid
74
+ */
75
+ async generateFromUrl(url) {
76
+ try {
77
+ new URL(url);
78
+ } catch {
79
+ throw new InvalidArgumentException(`Invalid URL: ${url}`);
80
+ }
81
+
82
+ const data = {
83
+ url: url,
84
+ };
85
+
86
+ const response = await this.#makeRequest('/pdfs/generate', data);
87
+
88
+ if (!response.data) {
89
+ throw new InvalidArgumentException('Invalid API response: missing data');
90
+ }
91
+
92
+ return Pdf.fromArray(response.data, this);
93
+ }
94
+
95
+ /**
96
+ * Get a PDF by its ID.
97
+ *
98
+ * @param {number} id The PDF ID
99
+ * @returns {Promise<Pdf>} PDF object containing PDF information
100
+ * @throws {InvalidArgumentException} If ID is invalid
101
+ */
102
+ async getPdf(id) {
103
+ if (id <= 0) {
104
+ throw new InvalidArgumentException(`Invalid PDF ID: ${id}`);
105
+ }
106
+
107
+ const response = await this.#makeGetRequest(`/pdfs/${id}`);
108
+
109
+ if (!response.data) {
110
+ throw new InvalidArgumentException('Invalid API response: missing data');
111
+ }
112
+
113
+ return Pdf.fromArray(response.data, this);
114
+ }
115
+
116
+ /**
117
+ * Process image files and return formatted array for API.
118
+ *
119
+ * @param {Array<{name: string, path: string, mimeType?: string}>} images Array of image inputs
120
+ * @returns {Array<{name: string, content: string, mime_type: string}>} Array of processed images
121
+ */
122
+ #processImages(images) {
123
+ const processed = [];
124
+
125
+ for (const image of images) {
126
+ if (!image.path || !image.name) {
127
+ continue;
128
+ }
129
+
130
+ const path = image.path;
131
+ const name = image.name;
132
+
133
+ if (!existsSync(path)) {
134
+ continue;
135
+ }
136
+
137
+ const content = readFileSync(path);
138
+ const contentBase64 = content.toString('base64');
139
+
140
+ // Detect mime type if not provided
141
+ const mimeType = image.mimeType ?? this.#detectMimeType(path);
142
+
143
+ processed.push({
144
+ name: name,
145
+ content: contentBase64,
146
+ mime_type: mimeType,
147
+ });
148
+ }
149
+
150
+ return processed;
151
+ }
152
+
153
+ /**
154
+ * Detect MIME type of a file based on extension.
155
+ *
156
+ * @param {string} filePath Path to the file
157
+ * @returns {string} MIME type
158
+ */
159
+ #detectMimeType(filePath) {
160
+ const extension = filePath.split('.').pop()?.toLowerCase() ?? '';
161
+ const mimeTypes = {
162
+ jpg: 'image/jpeg',
163
+ jpeg: 'image/jpeg',
164
+ png: 'image/png',
165
+ gif: 'image/gif',
166
+ webp: 'image/webp',
167
+ svg: 'image/svg+xml',
168
+ };
169
+
170
+ return mimeTypes[extension] ?? 'application/octet-stream';
171
+ }
172
+
173
+ /**
174
+ * Download a PDF from the API.
175
+ *
176
+ * @param {string} downloadUrl The download URL for the PDF
177
+ * @returns {Promise<Buffer>} PDF binary content as Buffer
178
+ */
179
+ async downloadPdf(downloadUrl) {
180
+ const response = await fetch(downloadUrl, {
181
+ headers: {
182
+ Authorization: `Bearer ${this.#apiToken}`,
183
+ },
184
+ });
185
+
186
+ if (!response.ok) {
187
+ throw new Error(`Failed to download PDF: ${response.statusText}`);
188
+ }
189
+
190
+ const arrayBuffer = await response.arrayBuffer();
191
+ return Buffer.from(arrayBuffer);
192
+ }
193
+
194
+ /**
195
+ * Make an HTTP POST request to the API.
196
+ *
197
+ * @param {string} endpoint API endpoint
198
+ * @param {Record<string, unknown>} data Request data
199
+ * @returns {Promise<{data?: {id: number, name: string, status: string, download_url: string, created_at: string}}>} Decoded JSON response
200
+ */
201
+ async #makeRequest(endpoint, data) {
202
+ const url = `${this.#baseUrl}${endpoint}`;
203
+
204
+ const response = await fetch(url, {
205
+ method: 'POST',
206
+ headers: {
207
+ Authorization: `Bearer ${this.#apiToken}`,
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ body: JSON.stringify(data),
211
+ });
212
+
213
+ if (!response.ok) {
214
+ const errorText = await response.text();
215
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
216
+ }
217
+
218
+ return await response.json();
219
+ }
220
+
221
+ /**
222
+ * Make an HTTP GET request to the API.
223
+ *
224
+ * @param {string} endpoint API endpoint
225
+ * @returns {Promise<{data?: {id: number, name: string, status: string, download_url: string, created_at: string}}>} Decoded JSON response
226
+ */
227
+ async #makeGetRequest(endpoint) {
228
+ const url = `${this.#baseUrl}${endpoint}`;
229
+
230
+ const response = await fetch(url, {
231
+ headers: {
232
+ Authorization: `Bearer ${this.#apiToken}`,
233
+ },
234
+ });
235
+
236
+ if (!response.ok) {
237
+ const errorText = await response.text();
238
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
239
+ }
240
+
241
+ return await response.json();
242
+ }
243
+ }
244
+
package/src/Pdf.js ADDED
@@ -0,0 +1,154 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { GeneratePDFs } from './GeneratePDFs.js';
3
+ import { InvalidArgumentException } from './exceptions/InvalidArgumentException.js';
4
+ import { RuntimeException } from './exceptions/RuntimeException.js';
5
+
6
+ export class Pdf {
7
+ #client;
8
+ #id;
9
+ #name;
10
+ #status;
11
+ #downloadUrl;
12
+ #createdAt;
13
+
14
+ constructor(id, name, status, downloadUrl, createdAt, client) {
15
+ this.#id = id;
16
+ this.#name = name;
17
+ this.#status = status;
18
+ this.#downloadUrl = downloadUrl;
19
+ this.#createdAt = createdAt;
20
+ this.#client = client;
21
+ }
22
+
23
+ /**
24
+ * Create a Pdf instance from API response data.
25
+ *
26
+ * @param {{id: number, name: string, status: string, download_url: string, created_at: string}} data API response data
27
+ * @param {GeneratePDFs} client The GeneratePDFs client instance
28
+ * @returns {Pdf} Pdf instance
29
+ */
30
+ static fromArray(data, client) {
31
+ if (!data.id || !data.name || !data.status || !data.download_url || !data.created_at) {
32
+ throw new InvalidArgumentException('Invalid PDF data structure');
33
+ }
34
+
35
+ // Parse the created_at date
36
+ let createdAt;
37
+ try {
38
+ createdAt = new Date(data.created_at);
39
+ if (isNaN(createdAt.getTime())) {
40
+ throw new InvalidArgumentException(`Invalid created_at format: ${data.created_at}`);
41
+ }
42
+ } catch (error) {
43
+ if (error instanceof InvalidArgumentException) {
44
+ throw error;
45
+ }
46
+ throw new InvalidArgumentException(`Invalid created_at format: ${data.created_at}`);
47
+ }
48
+
49
+ return new Pdf(
50
+ Number(data.id),
51
+ String(data.name),
52
+ String(data.status),
53
+ String(data.download_url),
54
+ createdAt,
55
+ client
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Get the PDF ID.
61
+ *
62
+ * @returns {number} PDF ID
63
+ */
64
+ getId() {
65
+ return this.#id;
66
+ }
67
+
68
+ /**
69
+ * Get the PDF name.
70
+ *
71
+ * @returns {string} PDF name
72
+ */
73
+ getName() {
74
+ return this.#name;
75
+ }
76
+
77
+ /**
78
+ * Get the PDF status.
79
+ *
80
+ * @returns {string} PDF status
81
+ */
82
+ getStatus() {
83
+ return this.#status;
84
+ }
85
+
86
+ /**
87
+ * Get the download URL.
88
+ *
89
+ * @returns {string} Download URL
90
+ */
91
+ getDownloadUrl() {
92
+ return this.#downloadUrl;
93
+ }
94
+
95
+ /**
96
+ * Get the creation date.
97
+ *
98
+ * @returns {Date} Creation date
99
+ */
100
+ getCreatedAt() {
101
+ return this.#createdAt;
102
+ }
103
+
104
+ /**
105
+ * Check if the PDF is ready for download.
106
+ *
107
+ * @returns {boolean} True if PDF is ready
108
+ */
109
+ isReady() {
110
+ return this.#status === 'completed';
111
+ }
112
+
113
+ /**
114
+ * Download the PDF content.
115
+ *
116
+ * @returns {Promise<Buffer>} PDF binary content as Buffer
117
+ * @throws {RuntimeException} If the PDF is not ready or download fails
118
+ */
119
+ async download() {
120
+ if (!this.isReady()) {
121
+ throw new RuntimeException(`PDF is not ready yet. Current status: ${this.#status}`);
122
+ }
123
+
124
+ return await this.#client.downloadPdf(this.#downloadUrl);
125
+ }
126
+
127
+ /**
128
+ * Download the PDF and save it to a file.
129
+ *
130
+ * @param {string} filePath Path where to save the PDF file
131
+ * @returns {Promise<boolean>} True on success
132
+ * @throws {RuntimeException} If the PDF is not ready or download fails
133
+ */
134
+ async downloadToFile(filePath) {
135
+ const content = await this.download();
136
+
137
+ try {
138
+ writeFileSync(filePath, content);
139
+ return true;
140
+ } catch (error) {
141
+ throw new RuntimeException(`Failed to write PDF to file: ${filePath}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Refresh the PDF data from the API.
147
+ *
148
+ * @returns {Promise<Pdf>} A new Pdf instance with updated data
149
+ */
150
+ async refresh() {
151
+ return await this.#client.getPdf(this.#id);
152
+ }
153
+ }
154
+
@@ -0,0 +1,385 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+ import { writeFileSync, unlinkSync, existsSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { GeneratePDFs } from '../GeneratePDFs.js';
6
+ import { Pdf } from '../Pdf.js';
7
+ import { InvalidArgumentException } from '../exceptions/InvalidArgumentException.js';
8
+
9
+ // Mock fetch globally
10
+ const mockFetch = jest.fn();
11
+ global.fetch = mockFetch;
12
+
13
+ describe('GeneratePDFs', () => {
14
+ let apiToken;
15
+ let baseUrl;
16
+
17
+ beforeEach(() => {
18
+ apiToken = 'test-api-token';
19
+ baseUrl = 'https://api.generatepdfs.com';
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ describe('connect', () => {
24
+ it('creates a new GeneratePDFs instance', () => {
25
+ const client = GeneratePDFs.connect(apiToken);
26
+ expect(client).toBeInstanceOf(GeneratePDFs);
27
+ });
28
+ });
29
+
30
+ describe('generateFromHtml', () => {
31
+ it('throws exception when HTML file does not exist', async () => {
32
+ const client = GeneratePDFs.connect(apiToken);
33
+
34
+ await expect(
35
+ client.generateFromHtml('/non/existent/file.html')
36
+ ).rejects.toThrow(InvalidArgumentException);
37
+ await expect(
38
+ client.generateFromHtml('/non/existent/file.html')
39
+ ).rejects.toThrow('HTML file not found or not readable');
40
+ });
41
+
42
+ it('throws exception when CSS file does not exist', async () => {
43
+ const client = GeneratePDFs.connect(apiToken);
44
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
45
+
46
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
47
+
48
+ try {
49
+ await expect(
50
+ client.generateFromHtml(htmlFile, '/non/existent/file.css')
51
+ ).rejects.toThrow(InvalidArgumentException);
52
+ await expect(
53
+ client.generateFromHtml(htmlFile, '/non/existent/file.css')
54
+ ).rejects.toThrow('CSS file not found or not readable');
55
+ } finally {
56
+ if (existsSync(htmlFile)) {
57
+ unlinkSync(htmlFile);
58
+ }
59
+ }
60
+ });
61
+
62
+ it('successfully generates PDF from HTML file', async () => {
63
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
64
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
65
+
66
+ const mockResponse = {
67
+ data: {
68
+ id: 123,
69
+ name: 'test.pdf',
70
+ status: 'pending',
71
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
72
+ created_at: '2024-01-01T12:00:00.000000Z',
73
+ },
74
+ };
75
+
76
+ mockFetch.mockResolvedValueOnce({
77
+ ok: true,
78
+ json: async () => mockResponse,
79
+ });
80
+
81
+ const client = GeneratePDFs.connect(apiToken);
82
+ const pdf = await client.generateFromHtml(htmlFile);
83
+
84
+ expect(pdf).toBeInstanceOf(Pdf);
85
+ expect(pdf.getId()).toBe(123);
86
+ expect(pdf.getName()).toBe('test.pdf');
87
+ expect(pdf.getStatus()).toBe('pending');
88
+
89
+ expect(mockFetch).toHaveBeenCalledWith(
90
+ `${baseUrl}/pdfs/generate`,
91
+ expect.objectContaining({
92
+ method: 'POST',
93
+ headers: expect.objectContaining({
94
+ Authorization: `Bearer ${apiToken}`,
95
+ 'Content-Type': 'application/json',
96
+ }),
97
+ })
98
+ );
99
+
100
+ if (existsSync(htmlFile)) {
101
+ unlinkSync(htmlFile);
102
+ }
103
+ });
104
+
105
+ it('includes CSS when provided', async () => {
106
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
107
+ const cssFile = join(tmpdir(), `test-${Date.now()}.css`);
108
+
109
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
110
+ writeFileSync(cssFile, 'body { color: red; }');
111
+
112
+ const mockResponse = {
113
+ data: {
114
+ id: 123,
115
+ name: 'test.pdf',
116
+ status: 'pending',
117
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
118
+ created_at: '2024-01-01T12:00:00.000000Z',
119
+ },
120
+ };
121
+
122
+ mockFetch.mockResolvedValueOnce({
123
+ ok: true,
124
+ json: async () => mockResponse,
125
+ });
126
+
127
+ const client = GeneratePDFs.connect(apiToken);
128
+ const pdf = await client.generateFromHtml(htmlFile, cssFile);
129
+
130
+ expect(pdf).toBeInstanceOf(Pdf);
131
+
132
+ const fetchCall = mockFetch.mock.calls[0];
133
+ const body = JSON.parse(fetchCall[1]?.body);
134
+ expect(body.html).toBeDefined();
135
+ expect(body.css).toBeDefined();
136
+
137
+ if (existsSync(htmlFile)) {
138
+ unlinkSync(htmlFile);
139
+ }
140
+ if (existsSync(cssFile)) {
141
+ unlinkSync(cssFile);
142
+ }
143
+ });
144
+
145
+ it('includes images when provided', async () => {
146
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
147
+ const imageFile = join(tmpdir(), `test-${Date.now()}.png`);
148
+
149
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
150
+ writeFileSync(imageFile, 'fake-image-content');
151
+
152
+ const mockResponse = {
153
+ data: {
154
+ id: 123,
155
+ name: 'test.pdf',
156
+ status: 'pending',
157
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
158
+ created_at: '2024-01-01T12:00:00.000000Z',
159
+ },
160
+ };
161
+
162
+ mockFetch.mockResolvedValueOnce({
163
+ ok: true,
164
+ json: async () => mockResponse,
165
+ });
166
+
167
+ const client = GeneratePDFs.connect(apiToken);
168
+ const pdf = await client.generateFromHtml(htmlFile, null, [
169
+ {
170
+ name: 'test.png',
171
+ path: imageFile,
172
+ },
173
+ ]);
174
+
175
+ expect(pdf).toBeInstanceOf(Pdf);
176
+
177
+ const fetchCall = mockFetch.mock.calls[0];
178
+ const body = JSON.parse(fetchCall[1]?.body);
179
+ expect(body.html).toBeDefined();
180
+ expect(body.images).toBeDefined();
181
+ expect(Array.isArray(body.images)).toBe(true);
182
+ expect(body.images.length).toBeGreaterThan(0);
183
+
184
+ if (existsSync(htmlFile)) {
185
+ unlinkSync(htmlFile);
186
+ }
187
+ if (existsSync(imageFile)) {
188
+ unlinkSync(imageFile);
189
+ }
190
+ });
191
+
192
+ it('throws exception when API response is invalid', async () => {
193
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
194
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
195
+
196
+ const mockResponse = {
197
+ // Missing 'data' key
198
+ };
199
+
200
+ const mockResponseObj = {
201
+ ok: true,
202
+ json: async () => mockResponse,
203
+ };
204
+
205
+ mockFetch.mockResolvedValueOnce(mockResponseObj);
206
+ mockFetch.mockResolvedValueOnce(mockResponseObj);
207
+
208
+ const client = GeneratePDFs.connect(apiToken);
209
+
210
+ await expect(client.generateFromHtml(htmlFile)).rejects.toThrow(InvalidArgumentException);
211
+ await expect(client.generateFromHtml(htmlFile)).rejects.toThrow('Invalid API response: missing data');
212
+
213
+ if (existsSync(htmlFile)) {
214
+ unlinkSync(htmlFile);
215
+ }
216
+ });
217
+
218
+ it('handles API errors', async () => {
219
+ const htmlFile = join(tmpdir(), `test-${Date.now()}.html`);
220
+ writeFileSync(htmlFile, '<html><body>Test</body></html>');
221
+
222
+ mockFetch.mockResolvedValueOnce({
223
+ ok: false,
224
+ status: 400,
225
+ statusText: 'Bad Request',
226
+ text: async () => 'Error message',
227
+ });
228
+
229
+ const client = GeneratePDFs.connect(apiToken);
230
+
231
+ await expect(client.generateFromHtml(htmlFile)).rejects.toThrow();
232
+
233
+ if (existsSync(htmlFile)) {
234
+ unlinkSync(htmlFile);
235
+ }
236
+ });
237
+ });
238
+
239
+ describe('generateFromUrl', () => {
240
+ it('throws exception for invalid URL', async () => {
241
+ const client = GeneratePDFs.connect(apiToken);
242
+
243
+ await expect(client.generateFromUrl('not-a-valid-url')).rejects.toThrow(InvalidArgumentException);
244
+ await expect(client.generateFromUrl('not-a-valid-url')).rejects.toThrow('Invalid URL');
245
+ });
246
+
247
+ it('successfully generates PDF from URL', async () => {
248
+ const mockResponse = {
249
+ data: {
250
+ id: 456,
251
+ name: 'url-example.com-2024-01-01-12-00-00.pdf',
252
+ status: 'pending',
253
+ download_url: 'https://api.generatepdfs.com/pdfs/456/download/token',
254
+ created_at: '2024-01-01T12:00:00.000000Z',
255
+ },
256
+ };
257
+
258
+ mockFetch.mockResolvedValueOnce({
259
+ ok: true,
260
+ json: async () => mockResponse,
261
+ });
262
+
263
+ const client = GeneratePDFs.connect(apiToken);
264
+ const pdf = await client.generateFromUrl('https://example.com');
265
+
266
+ expect(pdf).toBeInstanceOf(Pdf);
267
+ expect(pdf.getId()).toBe(456);
268
+ expect(pdf.getName()).toBe('url-example.com-2024-01-01-12-00-00.pdf');
269
+
270
+ const fetchCall = mockFetch.mock.calls[0];
271
+ const body = JSON.parse(fetchCall[1]?.body);
272
+ expect(body.url).toBe('https://example.com');
273
+ });
274
+ });
275
+
276
+ describe('getPdf', () => {
277
+ it('throws exception for invalid ID', async () => {
278
+ const client = GeneratePDFs.connect(apiToken);
279
+
280
+ await expect(client.getPdf(0)).rejects.toThrow(InvalidArgumentException);
281
+ await expect(client.getPdf(0)).rejects.toThrow('Invalid PDF ID: 0');
282
+
283
+ await expect(client.getPdf(-1)).rejects.toThrow(InvalidArgumentException);
284
+ await expect(client.getPdf(-1)).rejects.toThrow('Invalid PDF ID: -1');
285
+ });
286
+
287
+ it('successfully retrieves PDF by ID', async () => {
288
+ const mockResponse = {
289
+ data: {
290
+ id: 789,
291
+ name: 'retrieved.pdf',
292
+ status: 'completed',
293
+ download_url: 'https://api.generatepdfs.com/pdfs/789/download/token',
294
+ created_at: '2024-01-01T12:00:00.000000Z',
295
+ },
296
+ };
297
+
298
+ mockFetch.mockResolvedValueOnce({
299
+ ok: true,
300
+ json: async () => mockResponse,
301
+ });
302
+
303
+ const client = GeneratePDFs.connect(apiToken);
304
+ const pdf = await client.getPdf(789);
305
+
306
+ expect(pdf).toBeInstanceOf(Pdf);
307
+ expect(pdf.getId()).toBe(789);
308
+ expect(pdf.getName()).toBe('retrieved.pdf');
309
+ expect(pdf.getStatus()).toBe('completed');
310
+
311
+ expect(mockFetch).toHaveBeenCalledWith(
312
+ `${baseUrl}/pdfs/789`,
313
+ expect.objectContaining({
314
+ headers: expect.objectContaining({
315
+ Authorization: `Bearer ${apiToken}`,
316
+ }),
317
+ })
318
+ );
319
+ });
320
+
321
+ it('throws exception when API response is invalid', async () => {
322
+ const mockResponse = {
323
+ // Missing 'data' key
324
+ };
325
+
326
+ const mockResponseObj = {
327
+ ok: true,
328
+ json: async () => mockResponse,
329
+ };
330
+
331
+ mockFetch.mockResolvedValueOnce(mockResponseObj);
332
+ mockFetch.mockResolvedValueOnce(mockResponseObj);
333
+
334
+ const client = GeneratePDFs.connect(apiToken);
335
+
336
+ await expect(client.getPdf(123)).rejects.toThrow(InvalidArgumentException);
337
+ await expect(client.getPdf(123)).rejects.toThrow('Invalid API response: missing data');
338
+ });
339
+ });
340
+
341
+ describe('downloadPdf', () => {
342
+ it('successfully downloads PDF content', async () => {
343
+ const downloadUrl = 'https://api.generatepdfs.com/pdfs/123/download/token';
344
+ const pdfContent = '%PDF-1.4 fake pdf content';
345
+
346
+ mockFetch.mockResolvedValueOnce({
347
+ ok: true,
348
+ arrayBuffer: async () => {
349
+ const buffer = Buffer.from(pdfContent, 'utf-8');
350
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
351
+ },
352
+ });
353
+
354
+ const client = GeneratePDFs.connect(apiToken);
355
+ const content = await client.downloadPdf(downloadUrl);
356
+
357
+ expect(Buffer.isBuffer(content)).toBe(true);
358
+ expect(content.toString('utf-8')).toBe(pdfContent);
359
+
360
+ expect(mockFetch).toHaveBeenCalledWith(
361
+ downloadUrl,
362
+ expect.objectContaining({
363
+ headers: expect.objectContaining({
364
+ Authorization: `Bearer ${apiToken}`,
365
+ }),
366
+ })
367
+ );
368
+ });
369
+
370
+ it('throws exception when download fails', async () => {
371
+ const downloadUrl = 'https://api.generatepdfs.com/pdfs/123/download/token';
372
+
373
+ mockFetch.mockResolvedValueOnce({
374
+ ok: false,
375
+ status: 404,
376
+ statusText: 'Not Found',
377
+ });
378
+
379
+ const client = GeneratePDFs.connect(apiToken);
380
+
381
+ await expect(client.downloadPdf(downloadUrl)).rejects.toThrow();
382
+ });
383
+ });
384
+ });
385
+
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+ import { writeFileSync, unlinkSync, existsSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { GeneratePDFs } from '../GeneratePDFs.js';
6
+ import { Pdf } from '../Pdf.js';
7
+ import { InvalidArgumentException } from '../exceptions/InvalidArgumentException.js';
8
+ import { RuntimeException } from '../exceptions/RuntimeException.js';
9
+
10
+ // Mock fetch globally
11
+ const mockFetch = jest.fn();
12
+ global.fetch = mockFetch;
13
+
14
+ describe('Pdf', () => {
15
+ let client;
16
+ let apiToken;
17
+
18
+ beforeEach(() => {
19
+ apiToken = 'test-api-token';
20
+ client = GeneratePDFs.connect(apiToken);
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ describe('fromArray', () => {
25
+ it('creates Pdf instance from valid data', () => {
26
+ const data = {
27
+ id: 123,
28
+ name: 'test.pdf',
29
+ status: 'completed',
30
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
31
+ created_at: '2024-01-01T12:00:00.000000Z',
32
+ };
33
+
34
+ const pdf = Pdf.fromArray(data, client);
35
+
36
+ expect(pdf).toBeInstanceOf(Pdf);
37
+ expect(pdf.getId()).toBe(123);
38
+ expect(pdf.getName()).toBe('test.pdf');
39
+ expect(pdf.getStatus()).toBe('completed');
40
+ expect(pdf.getDownloadUrl()).toBe('https://api.generatepdfs.com/pdfs/123/download/token');
41
+ });
42
+
43
+ it('throws exception when required fields are missing', () => {
44
+ const data = {
45
+ id: 123,
46
+ // Missing other required fields
47
+ };
48
+
49
+ expect(() => Pdf.fromArray(data, client)).toThrow(InvalidArgumentException);
50
+ expect(() => Pdf.fromArray(data, client)).toThrow('Invalid PDF data structure');
51
+ });
52
+
53
+ it('throws exception when created_at format is invalid', () => {
54
+ const data = {
55
+ id: 123,
56
+ name: 'test.pdf',
57
+ status: 'completed',
58
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
59
+ created_at: 'invalid-date-format',
60
+ };
61
+
62
+ expect(() => Pdf.fromArray(data, client)).toThrow(InvalidArgumentException);
63
+ expect(() => Pdf.fromArray(data, client)).toThrow('Invalid created_at format');
64
+ });
65
+ });
66
+
67
+ describe('getters', () => {
68
+ it('return correct values', () => {
69
+ const data = {
70
+ id: 456,
71
+ name: 'document.pdf',
72
+ status: 'pending',
73
+ download_url: 'https://api.generatepdfs.com/pdfs/456/download/token',
74
+ created_at: '2024-01-01T12:00:00.000000Z',
75
+ };
76
+
77
+ const pdf = Pdf.fromArray(data, client);
78
+
79
+ expect(pdf.getId()).toBe(456);
80
+ expect(pdf.getName()).toBe('document.pdf');
81
+ expect(pdf.getStatus()).toBe('pending');
82
+ expect(pdf.getDownloadUrl()).toBe('https://api.generatepdfs.com/pdfs/456/download/token');
83
+ expect(pdf.getCreatedAt()).toBeInstanceOf(Date);
84
+ });
85
+ });
86
+
87
+ describe('isReady', () => {
88
+ it('returns true when status is completed', () => {
89
+ const data = {
90
+ id: 123,
91
+ name: 'test.pdf',
92
+ status: 'completed',
93
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
94
+ created_at: '2024-01-01T12:00:00.000000Z',
95
+ };
96
+
97
+ const pdf = Pdf.fromArray(data, client);
98
+
99
+ expect(pdf.isReady()).toBe(true);
100
+ });
101
+
102
+ it('returns false when status is not completed', () => {
103
+ const data = {
104
+ id: 123,
105
+ name: 'test.pdf',
106
+ status: 'pending',
107
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
108
+ created_at: '2024-01-01T12:00:00.000000Z',
109
+ };
110
+
111
+ const pdf = Pdf.fromArray(data, client);
112
+
113
+ expect(pdf.isReady()).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('download', () => {
118
+ it('throws exception when PDF is not ready', async () => {
119
+ const data = {
120
+ id: 123,
121
+ name: 'test.pdf',
122
+ status: 'pending',
123
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
124
+ created_at: '2024-01-01T12:00:00.000000Z',
125
+ };
126
+
127
+ const pdf = Pdf.fromArray(data, client);
128
+
129
+ await expect(pdf.download()).rejects.toThrow(RuntimeException);
130
+ await expect(pdf.download()).rejects.toThrow('PDF is not ready yet');
131
+ });
132
+
133
+ it('successfully downloads PDF content', async () => {
134
+ const data = {
135
+ id: 123,
136
+ name: 'test.pdf',
137
+ status: 'completed',
138
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
139
+ created_at: '2024-01-01T12:00:00.000000Z',
140
+ };
141
+
142
+ const pdfContent = '%PDF-1.4 fake pdf content';
143
+
144
+ mockFetch.mockResolvedValueOnce({
145
+ ok: true,
146
+ arrayBuffer: async () => {
147
+ const buffer = Buffer.from(pdfContent, 'utf-8');
148
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
149
+ },
150
+ });
151
+
152
+ const pdf = Pdf.fromArray(data, client);
153
+ const content = await pdf.download();
154
+
155
+ expect(Buffer.isBuffer(content)).toBe(true);
156
+ expect(content.toString('utf-8')).toBe(pdfContent);
157
+ });
158
+ });
159
+
160
+ describe('downloadToFile', () => {
161
+ it('successfully saves PDF to file', async () => {
162
+ const data = {
163
+ id: 123,
164
+ name: 'test.pdf',
165
+ status: 'completed',
166
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
167
+ created_at: '2024-01-01T12:00:00.000000Z',
168
+ };
169
+
170
+ const pdfContent = '%PDF-1.4 fake pdf content';
171
+ const tempFile = join(tmpdir(), `test-${Date.now()}.pdf`);
172
+
173
+ mockFetch.mockResolvedValueOnce({
174
+ ok: true,
175
+ arrayBuffer: async () => {
176
+ const buffer = Buffer.from(pdfContent, 'utf-8');
177
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
178
+ },
179
+ });
180
+
181
+ const pdf = Pdf.fromArray(data, client);
182
+
183
+ try {
184
+ const result = await pdf.downloadToFile(tempFile);
185
+
186
+ expect(result).toBe(true);
187
+ expect(existsSync(tempFile)).toBe(true);
188
+ // Note: We can't easily read the file back in the test environment
189
+ // but the file existence check is sufficient
190
+ } finally {
191
+ if (existsSync(tempFile)) {
192
+ unlinkSync(tempFile);
193
+ }
194
+ }
195
+ });
196
+ });
197
+
198
+ describe('fromArray with different status values', () => {
199
+ it('handles different status values', () => {
200
+ const statuses = ['pending', 'processing', 'completed', 'failed'];
201
+
202
+ for (const status of statuses) {
203
+ const data = {
204
+ id: 123,
205
+ name: 'test.pdf',
206
+ status: status,
207
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
208
+ created_at: '2024-01-01T12:00:00.000000Z',
209
+ };
210
+
211
+ const pdf = Pdf.fromArray(data, client);
212
+
213
+ expect(pdf.getStatus()).toBe(status);
214
+ expect(pdf.isReady()).toBe(status === 'completed');
215
+ }
216
+ });
217
+ });
218
+
219
+ describe('refresh', () => {
220
+ it('successfully updates PDF data', async () => {
221
+ const initialData = {
222
+ id: 123,
223
+ name: 'test.pdf',
224
+ status: 'pending',
225
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/token',
226
+ created_at: '2024-01-01T12:00:00.000000Z',
227
+ };
228
+
229
+ const pdf = Pdf.fromArray(initialData, client);
230
+
231
+ // Verify initial state
232
+ expect(pdf.getStatus()).toBe('pending');
233
+
234
+ const mockResponse = {
235
+ data: {
236
+ id: 123,
237
+ name: 'test.pdf',
238
+ status: 'completed',
239
+ download_url: 'https://api.generatepdfs.com/pdfs/123/download/new-token',
240
+ created_at: '2024-01-01T12:00:00.000000Z',
241
+ },
242
+ };
243
+
244
+ mockFetch.mockResolvedValueOnce({
245
+ ok: true,
246
+ json: async () => mockResponse,
247
+ });
248
+
249
+ // Refresh the PDF
250
+ const refreshedPdf = await pdf.refresh();
251
+
252
+ // Verify refreshed state
253
+ expect(refreshedPdf).toBeInstanceOf(Pdf);
254
+ expect(refreshedPdf.getId()).toBe(123);
255
+ expect(refreshedPdf.getStatus()).toBe('completed');
256
+ expect(refreshedPdf.getDownloadUrl()).toBe('https://api.generatepdfs.com/pdfs/123/download/new-token');
257
+ });
258
+ });
259
+ });
260
+
@@ -0,0 +1,8 @@
1
+ export class InvalidArgumentException extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'InvalidArgumentException';
5
+ Object.setPrototypeOf(this, InvalidArgumentException.prototype);
6
+ }
7
+ }
8
+
@@ -0,0 +1,8 @@
1
+ export class RuntimeException extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'RuntimeException';
5
+ Object.setPrototypeOf(this, RuntimeException.prototype);
6
+ }
7
+ }
8
+
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { GeneratePDFs } from './GeneratePDFs.js';
2
+ export { Pdf } from './Pdf.js';
3
+ export { InvalidArgumentException } from './exceptions/InvalidArgumentException.js';
4
+ export { RuntimeException } from './exceptions/RuntimeException.js';
5
+