Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into release/v4.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
MrRefactoring committed Jan 11, 2025
2 parents 6d91b99 + 4dc5acb commit 883a6e4
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 42 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@
- **`ServiceRegistry`**: Access and manage attributes related to Jira Service Management’s service registry, which helps organize and maintain services ([documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-service-registry/#api-group-service-registry)).
- **`TeamsInPlan`**: Configure settings for Atlassian and custom teams within advanced roadmaps plans, including creating, updating, and deleting team configurations ([documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-teams-in-plan/#api-group-teams-in-plan)).

### 4.0.6

- **#347:** Fixed an issue with adding attachments of type `Readable` or `ReadableStream` (e.g., `fs.createReadStream`). Thanks to [Lunatic174](https://github.com/Lunatic174) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/347).

---

### 4.0.5

- **#344:** Replaced the `mime-types` library with `mime` to ensure browser compatibility, as `mime-types` relies on the `path` module from Node.js. Thanks to [kang](https://github.com/kang8) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/344) and proposing the fix.

---

### 4.0.4

- **#320:** Resolved a tree-shaking issue where importing a single client would still include all clients in the output bundle when using bundlers. Now, only the required client code is included. Thanks to [Nao Yonashiro](https://github.com/orisano) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/320) and proposing a fix.
Expand Down Expand Up @@ -72,12 +80,16 @@
console.log(attachment[0].mimeType); // Will be 'application/typescript'
```

---

### 4.0.3

- **Bug Fix:** Fixed an issue with the `Users.createUser` method by adding the required `products` property. Thanks to [Appelberg-s](https://github.com/Appelberg-s) for the [fix](https://github.com/MrRefactoring/jira.js/commit/362918093c20036049db334743e2a0f5f41cbcd4#diff-6960050bc2a3d9ffad9eb5e307145969dc4a38eb5434eebf39da545fd18e01b7R12).
- **Documentation Update:** Corrected an error in `README.md`. Thanks to [Maurice de Bruyn](https://github.com/ueberBrot) for the [contribution](https://github.com/MrRefactoring/jira.js/commit/fb6151e1a0c7953b9447aaaf99caea5c2f93bb96).
- **Dependencies:** Updated all dependencies to their latest versions.

---

### 4.0.2

- `getAllProjects` in README and examples replaced to `searchProjects`. Thanks to [Alexander Pivovarov](https://github.com/bladerunner2020) for reporting [the issue](https://github.com/MrRefactoring/jira.js/issues/323).
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"test": "npm run test:unit && npm run test:integration",
"test:unit": "vitest run tests/unit --maxWorkers=8 --sequence.concurrent",
"test:integration": "vitest run tests/integration --bail=1 --no-file-parallelism --max-concurrency 1 -c vitest.config.mts --hookTimeout 100000 --testTimeout 100000",
"replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2",
"replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2 && npm run replace:attachment:serviceDesk",
"replace:permissions:version2": "grep -rl \"(#permissions)\" ./src/version2 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#permissions)/g'",
"replace:permissions:version3": "grep -rl \"(#permissions)\" ./src/version3 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#permissions)/g'",
"replace:pagination:version2": "grep -rl \"(#pagination)\" ./src/version2 | xargs sed -i '' 's/(#pagination)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#pagination)/g'",
Expand All @@ -50,6 +50,7 @@
"replace:ordering:version3": "grep -rl \"(#ordering)\" ./src/version3 | xargs sed -i '' 's/(#ordering)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#ordering)/g'",
"replace:groupMember:version2": "grep -rl \"(#api-rest-api-2-group-member-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-group-member-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-groups\\/#api-rest-api-2-group-member-get)/g'",
"replace:workflowPaginated:version2": "grep -rl \"(#api-rest-api-2-workflow-search-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-workflow-search-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-workflows\\/#api-rest-api-2-workflow-search-get)/g'",
"replace:attachment:serviceDesk": "grep -rl \"(#api-request-issueIdOrKey-attachment-post)\" ./src/serviceDesk | xargs sed -i '' 's/(#api-request-issueIdOrKey-attachment-post)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/service-desk\\/rest\\/api-group-servicedesk\\/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post)/g'",
"code:formatting": "npm run replace:all && npm run prettier && npm run lint:fix"
},
"devDependencies": {
Expand All @@ -66,7 +67,7 @@
"prettier-plugin-jsdoc": "^1.3.2",
"sinon": "^18.0.1",
"typedoc": "^0.27.6",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
},
Expand Down
4 changes: 3 additions & 1 deletion src/serviceDesk/parameters/attachTemporaryFile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Readable } from 'node:stream';

/**
* Represents an attachment to be temporarily attached to a Service Desk.
*
Expand Down Expand Up @@ -35,7 +37,7 @@ export interface Attachment {
* const fileContent = Buffer.from('Example content here');
* ```
*/
file: Buffer | ReadableStream | string | Blob | File;
file: Buffer | ReadableStream | Readable | string | Blob | File;

/**
* Optional MIME type of the attachment. Example values include:
Expand Down
98 changes: 90 additions & 8 deletions src/serviceDesk/serviceDesk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FormData, File } from 'formdata-node';
import type { Mime } from 'mime' with { 'resolution-mode': 'import' };
import * as Models from './models';
import * as Parameters from './parameters';
import { Callback } from '../callback';
Expand Down Expand Up @@ -85,7 +86,7 @@ export class ServiceDesk {
/**
* This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a
* customer request using
* [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post).
* [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post).
*
* **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service
* desk.
Expand All @@ -100,7 +101,7 @@ export class ServiceDesk {
/**
* This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a
* customer request using
* [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post).
* [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post).
*
* **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service
* desk.
Expand All @@ -118,14 +119,24 @@ export class ServiceDesk {

const { default: mime } = await import('mime');

attachments.forEach(attachment => {
const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined);
const file = Buffer.isBuffer(attachment.file)
? new File([attachment.file], attachment.filename, { type: mimeType })
: attachment.file;
let Readable: typeof import('stream').Readable | undefined;

if (typeof window === 'undefined') {
const { Readable: NodeReadable } = await import('stream');

Readable = NodeReadable;
}

// eslint-disable-next-line no-restricted-syntax
for await (const attachment of attachments) {
const file = await this._convertToFile(attachment, mime, Readable);

if (!(file instanceof File || file instanceof Blob)) {
throw new Error(`Unsupported file type for attachment: ${typeof file}`);
}

formData.append('file', file, attachment.filename);
});
}

const config: RequestConfig = {
url: `/rest/servicedeskapi/servicedesk/${parameters.serviceDeskId}/attachTemporaryFile`,
Expand Down Expand Up @@ -808,4 +819,75 @@ export class ServiceDesk {

return this.client.sendRequest(config, callback);
}

private async _convertToFile(
attachment: Parameters.Attachment,
mime: Mime,
Readable?: typeof import('stream').Readable,
): Promise<File | Blob> {
const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined);

if (attachment.file instanceof Blob || attachment.file instanceof File) {
return attachment.file;
}

if (typeof attachment.file === 'string') {
return new File([attachment.file], attachment.filename, { type: mimeType });
}

if (Readable && attachment.file instanceof Readable) {
return this._streamToBlob(attachment.file, attachment.filename, mimeType);
}

if (attachment.file instanceof ReadableStream) {
return this._streamToBlob(attachment.file, attachment.filename, mimeType);
}

if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) {
return new File([attachment.file], attachment.filename, { type: mimeType });
}

throw new Error('Unsupported attachment file type.');
}

private async _streamToBlob(
stream: import('stream').Readable | ReadableStream,
filename: string,
mimeType?: string,
): Promise<File> {
if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) {
return new Promise((resolve, reject) => {
const chunks: Uint8Array[] = [];

stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => {
const blob = new Blob(chunks, { type: mimeType });

resolve(new File([blob], filename, { type: mimeType }));
});
stream.on('error', reject);
});
}

if (stream instanceof ReadableStream) {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];

let done = false;

while (!done) {
// eslint-disable-next-line no-await-in-loop
const { value, done: streamDone } = await reader.read();

if (value) chunks.push(value);
done = streamDone;
}

const blob = new Blob(chunks, { type: mimeType });

return new File([blob], filename, { type: mimeType });
}

throw new Error('Unsupported stream type.');
}
}
106 changes: 88 additions & 18 deletions src/version2/issueAttachments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FormData, File } from 'formdata-node';
import type { Mime } from 'mime' with { 'resolution-mode': 'import' };
import * as Models from './models';
import * as Parameters from './parameters';
import { Client } from '../clients';
Expand Down Expand Up @@ -379,12 +380,6 @@ export class IssueAttachments {
* Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC
* 1867](https://www.ietf.org/rfc/rfc1867.txt)).
*
* Note that:
*
* - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special
* headers](#special-request-headers) for more information.
* - The name of the multipart/form-data parameter that contains the attachments must be `file`.
*
* This operation can be accessed anonymously.
*
* **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:**
Expand All @@ -402,12 +397,6 @@ export class IssueAttachments {
* Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC
* 1867](https://www.ietf.org/rfc/rfc1867.txt)).
*
* Note that:
*
* - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special
* headers](#special-request-headers) for more information.
* - The name of the multipart/form-data parameter that contains the attachments must be `file`.
*
* This operation can be accessed anonymously.
*
* **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:**
Expand All @@ -427,14 +416,24 @@ export class IssueAttachments {

const { default: mime } = await import('mime');

attachments.forEach(attachment => {
const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined);
const file = Buffer.isBuffer(attachment.file)
? new File([attachment.file], attachment.filename, { type: mimeType })
: attachment.file;
let Readable: typeof import('stream').Readable | undefined;

if (typeof window === 'undefined') {
const { Readable: NodeReadable } = await import('stream');

Readable = NodeReadable;
}

// eslint-disable-next-line no-restricted-syntax
for await (const attachment of attachments) {
const file = await this._convertToFile(attachment, mime, Readable);

if (!(file instanceof File || file instanceof Blob)) {
throw new Error(`Unsupported file type for attachment: ${typeof file}`);
}

formData.append('file', file, attachment.filename);
});
}

const config: RequestConfig = {
url: `/rest/api/2/issue/${parameters.issueIdOrKey}/attachments`,
Expand All @@ -450,4 +449,75 @@ export class IssueAttachments {

return this.client.sendRequest(config, callback);
}

private async _convertToFile(
attachment: Parameters.Attachment,
mime: Mime,
Readable?: typeof import('stream').Readable,
): Promise<File | Blob> {
const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined);

if (attachment.file instanceof Blob || attachment.file instanceof File) {
return attachment.file;
}

if (typeof attachment.file === 'string') {
return new File([attachment.file], attachment.filename, { type: mimeType });
}

if (Readable && attachment.file instanceof Readable) {
return this._streamToBlob(attachment.file, attachment.filename, mimeType);
}

if (attachment.file instanceof ReadableStream) {
return this._streamToBlob(attachment.file, attachment.filename, mimeType);
}

if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) {
return new File([attachment.file], attachment.filename, { type: mimeType });
}

throw new Error('Unsupported attachment file type.');
}

private async _streamToBlob(
stream: import('stream').Readable | ReadableStream,
filename: string,
mimeType?: string,
): Promise<File> {
if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) {
return new Promise((resolve, reject) => {
const chunks: Uint8Array[] = [];

stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => {
const blob = new Blob(chunks, { type: mimeType });

resolve(new File([blob], filename, { type: mimeType }));
});
stream.on('error', reject);
});
}

if (stream instanceof ReadableStream) {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];

let done = false;

while (!done) {
// eslint-disable-next-line no-await-in-loop
const { value, done: streamDone } = await reader.read();

if (value) chunks.push(value);
done = streamDone;
}

const blob = new Blob(chunks, { type: mimeType });

return new File([blob], filename, { type: mimeType });
}

throw new Error('Unsupported stream type.');
}
}
4 changes: 3 additions & 1 deletion src/version2/parameters/addAttachment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Readable } from 'node:stream';

/**
* Represents an attachment to be added to an issue.
*
Expand Down Expand Up @@ -35,7 +37,7 @@ export interface Attachment {
* const fileContent = fs.readFileSync('./document.pdf');
* ```
*/
file: Buffer | ReadableStream | string | Blob | File;
file: Buffer | ReadableStream | Readable | string | Blob | File;

/**
* Optional MIME type of the attachment. Example values include:
Expand Down
Loading

0 comments on commit 883a6e4

Please sign in to comment.