@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 +28 -0
- package/README.md +165 -0
- package/package.json +40 -0
- package/src/GeneratePDFs.js +244 -0
- package/src/Pdf.js +154 -0
- package/src/__tests__/GeneratePDFs.test.js +385 -0
- package/src/__tests__/Pdf.test.js +260 -0
- package/src/exceptions/InvalidArgumentException.js +8 -0
- package/src/exceptions/RuntimeException.js +8 -0
- package/src/index.js +5 -0
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
|
+
|