Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Better types and handling for different FormData implementations #405

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dist/Classes/Validations/multipleValidation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ export declare class MultipleValidationJob implements MultipleValidationJobResul
}
export default class MultipleValidationClient extends NavigationThruPages<MultipleValidationJobsListResult> implements IMultipleValidationClient {
request: Request;
private attachmentsHandler;
constructor(request: Request);
private handleResponse;
protected parseList(response: MultipleValidationJobsListResponse): MultipleValidationJobsListResult;
list(query?: MultipleValidationJobsListQuery): Promise<MultipleValidationJobsListResult>;
get(listId: string): Promise<MultipleValidationJob>;
private convertToExpectedShape;
create(listId: string, data: MultipleValidationCreationData): Promise<CreatedMultipleValidationJob>;
destroy(listId: string): Promise<CanceledMultipleValidationJob>;
}
27 changes: 27 additions & 0 deletions dist/Classes/common/AttachmentsHandler.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import { Readable } from 'stream';
import { CustomFile, CustomFileData } from '../../Types';
import { AttachmentInfo, StreamValue } from '../../Types/Common/Attachments';
declare class BlobFromStream {
private _stream;
size: number;
constructor(stream: Readable, size: number);
stream(): Readable;
get [Symbol.toStringTag](): string;
}
declare class AttachmentsHandler {
private getAttachmentOptions;
private getFileInfo;
private getCustomFileInfo;
private getBufferInfo;
isStream(data: unknown): data is StreamValue;
isCustomFile(obj: unknown): obj is CustomFile;
isBrowserFile(obj: unknown): obj is File;
isBuffer(data: unknown): data is Buffer;
getAttachmentInfo(attachment: CustomFile | File | string | CustomFileData): AttachmentInfo;
convertToFDexpectedShape(userProvidedValue: CustomFile | File | string | CustomFileData): string | Blob | Buffer | NodeJS.ReadableStream | (CustomFile & StreamValue);
getBlobFromStream(stream: Readable, size: number): BlobFromStream;
}
export default AttachmentsHandler;
1 change: 1 addition & 0 deletions dist/Classes/common/Error.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export default class APIError extends Error implements APIErrorType {
stack: string;
details: string;
type: string;
static getUserDataError(statusText: string, message: string): APIError;
constructor({ status, statusText, message, body }: APIErrorOptions);
}
13 changes: 8 additions & 5 deletions dist/Classes/common/FormDataBuilder.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as NodeFormData from 'form-data';
import { InputFormData } from '../../Types/Common';
import { FormDataInput, InputFormData } from '../../Types/Common';
import { MimeMessage } from '../../Types';
declare class FormDataBuilder {
private FormDataConstructor;
private fileKeys;
private attachmentsHandler;
constructor(FormDataConstructor: InputFormData);
createFormData(data: any): NodeFormData | FormData;
private isFormDataPackage;
private getAttachmentOptions;
createFormData(data: FormDataInput): NodeFormData | FormData;
private addMimeDataToFD;
isMIME(data: unknown): data is MimeMessage;
private isFormDataPackage;
private isMessageAttachment;
private addFilesToFD;
private isStream;
private addCommonPropertyToFD;
}
export default FormDataBuilder;
10 changes: 5 additions & 5 deletions dist/Classes/common/Request.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as NodeFormData from 'form-data';
import { RequestOptions, InputFormData, APIResponse, IpPoolDeleteData } from '../../Types';
import { RequestOptions, InputFormData, APIResponse, IpPoolDeleteData, FormDataInput } from '../../Types';
declare class Request {
private username;
private key;
Expand All @@ -20,10 +20,10 @@ declare class Request {
command(method: string, url: string, data?: Record<string, unknown> | Record<string, unknown>[] | string | NodeFormData | FormData, options?: Record<string, unknown>, addDefaultHeaders?: boolean): Promise<APIResponse>;
get(url: string, query?: Record<string, unknown> | Array<Array<string>>, options?: Record<string, unknown>): Promise<APIResponse>;
post(url: string, data?: Record<string, unknown> | string, options?: Record<string, unknown>): Promise<APIResponse>;
postWithFD(url: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<APIResponse>;
putWithFD(url: string, data: Record<string, unknown>): Promise<APIResponse>;
patchWithFD(url: string, data: Record<string, unknown>): Promise<APIResponse>;
put(url: string, data?: Record<string, unknown> | string, options?: Record<string, unknown>): Promise<APIResponse>;
postWithFD(url: string, data: FormDataInput): Promise<APIResponse>;
putWithFD(url: string, data: FormDataInput): Promise<APIResponse>;
patchWithFD(url: string, data: FormDataInput): Promise<APIResponse>;
put(url: string, data?: FormDataInput | string, options?: Record<string, unknown>): Promise<APIResponse>;
delete(url: string, data?: IpPoolDeleteData): Promise<APIResponse>;
}
export default Request;
12 changes: 12 additions & 0 deletions dist/Types/Common/Attachments.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type NodePipeFunction = (destination: WritableStream, options?: {
end?: boolean;
}) => void;
export type BrowserPipeFunction = (destination: WritableStream) => void;
export type StreamValue = {
pipe: NodePipeFunction | BrowserPipeFunction;
};
export type AttachmentInfo = {
filename?: string;
contentType?: string;
knownLength?: number;
};
10 changes: 8 additions & 2 deletions dist/Types/Common/FormData.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as NodeFormData from 'form-data';
import { FormDataInputValue } from '../Messages';
export type FormDataOptions = {
[key: string]: any;
[key: string]: NodeFormData;
};
export type InputFormData = {
new (options?: HTMLFormElement | FormDataOptions): NodeFormData | FormData;
new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
} | {
new (options?: FormDataOptions): NodeFormData;
};
export type FormDataInput = {
[key: string]: FormDataInputValue;
};
18 changes: 15 additions & 3 deletions dist/Types/Messages/Messages.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="node" />
/// <reference types="node" />
/**
* Ensures the object has least one key present and not undefined
*
Expand All @@ -7,6 +8,17 @@
export type AtLeastOneKeyPresent<Object_, Keys extends keyof Object_ = keyof Object_> = Pick<Object_, Exclude<keyof Object_, Keys>> & {
[K in Keys]-?: Required<Pick<Object_, K>> & Partial<Pick<Object_, Exclude<Keys, K>>>;
}[Keys];
export type MimeMessage = string | Blob | Buffer | NodeJS.ReadableStream;
export type CustomFileData = string | Blob | File | Buffer | NodeJS.ReadableStream;
export type CustomFile = {
data: CustomFileData;
filename?: string;
contentType?: string;
knownLength?: number;
[key: string]: unknown;
};
export type MessageAttachment = CustomFile | CustomFile[] | File | File[] | string | CustomFileData | CustomFileData[];
export type FormDataInputValue = MimeMessage | CustomFileData | string | string[] | boolean | MessageAttachment | undefined | number;
export type MailgunMessageContent = AtLeastOneKeyPresent<{
/**
* Body of the message. (text version)
Expand All @@ -19,7 +31,7 @@ export type MailgunMessageContent = AtLeastOneKeyPresent<{
/**
* Body of the message. (MIME version)
*/
message?: string | Buffer | Blob;
message?: MimeMessage;
/**
* Name of a template stored via [template API](https://documentation.mailgun.com/en/latest/api-templates.html#api-templates). See [Templates](https://documentation.mailgun.com/en/latest/user_manual.html#templating) for more information
*/
Expand Down Expand Up @@ -57,7 +69,7 @@ export type MailgunMessageData = MailgunMessageContent & {
*
* **Important:** You must use `multipart/form-data` encoding when sending attachments.
*/
attachment?: any;
attachment?: MessageAttachment;
/**
* Attachment with `inline` disposition. Can be used to send inline images (see example).
*
Expand Down Expand Up @@ -163,7 +175,7 @@ export type MailgunMessageData = MailgunMessageContent & {
* `v:` prefix followed by an arbitrary name allows to attach a custom JSON data to the message. See [Attaching Data to Messages](https://documentation.mailgun.com/en/latest/user_manual.html#manual-customdata) for more information.
*/
'v:my-var'?: string;
[key: string]: unknown;
[key: string]: FormDataInputValue;
};
export type MessagesSendAPIResponse = {
status: number;
Expand Down
7 changes: 3 additions & 4 deletions dist/Types/Validations/MultipleValidation.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PagesList, ParsedPagesList } from '../Common';
import { CustomFile, CustomFileData } from '../Messages';
export type MultipleValidationJobData = {
created_at: number;
id: string;
Expand Down Expand Up @@ -57,12 +58,10 @@ export type CreatedMultipleValidationJob = {
message: string;
};
export type MultipleValidationCreationData = {
file: Record<string, unknown>;
[key: string]: unknown | undefined;
file: CustomFileData | CustomFile;
};
export type MultipleValidationCreationDataUpdated = {
multipleValidationFile: Record<string, unknown>;
[key: string]: unknown | undefined;
multipleValidationFile: CustomFileData | CustomFile;
};
export type MultipleValidationJobsListResult = {
jobs: MultipleValidationJobResult[];
Expand Down
14,692 changes: 14,689 additions & 3 deletions dist/mailgun.node.js

Large diffs are not rendered by default.

9,935 changes: 9,932 additions & 3 deletions dist/mailgun.web.js

Large diffs are not rendered by default.

12 changes: 2 additions & 10 deletions lib/Classes/Domains/domainsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {

import { APIResponse } from '../../Types/Common/ApiResponse';
import APIError from '../common/Error';
import { APIErrorOptions } from '../../Types/Common';

import Request from '../common/Request';

import DomainCredentialsClient from './domainsCredentials';
Expand Down Expand Up @@ -168,7 +166,7 @@ export default class DomainsClient implements IDomainsClient {
data: OpenTrackingInfo | ClickTrackingInfo | UnsubscribeTrackingInfo
): Promise<UpdatedOpenTracking> {
if (typeof data?.active === 'boolean') {
throw new APIError({ status: 400, statusText: 'Received boolean value for active property', body: { message: 'Property "active" must contain string value.' } } as APIErrorOptions);
throw APIError.getUserDataError('Received boolean value for active property', 'Property "active" must contain string value.');
}
return this.request.putWithFD(urljoin('/v3/domains', domain, 'tracking', type), data)
.then((res : APIResponse) => this._parseTrackingUpdate(res as UpdateDomainTrackingResponse));
Expand Down Expand Up @@ -196,13 +194,7 @@ export default class DomainsClient implements IDomainsClient {
unlinkIpPoll(domain: string, replacement: ReplacementForPool): Promise<APIResponse> {
let searchParams = '';
if (replacement.pool_id && replacement.ip) {
throw new APIError(
{
status: 400,
statusText: 'Too much data for replacement',
body: { message: 'Please specify either pool_id or ip (not both)' }
} as APIErrorOptions
);
throw APIError.getUserDataError('Too much data for replacement', 'Please specify either pool_id or ip (not both)');
} else if (replacement.pool_id) {
searchParams = `?pool_id=${replacement.pool_id}`;
} else if (replacement.ip) {
Expand Down
6 changes: 1 addition & 5 deletions lib/Classes/Messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import APIError from './common/Error';
import {
APIErrorOptions,
MailgunMessageData,
MessagesSendAPIResponse,
MessagesSendResult
Expand Down Expand Up @@ -28,10 +27,7 @@ export default class MessagesClient implements IMessagesClient {
]);

if (!data || Object.keys(data).length === 0) {
throw new APIError({
status: 400,
message: 'Message data object can not be empty'
} as APIErrorOptions);
throw APIError.getUserDataError('Message data object can not be empty', 'Message data object can not be empty');
}
return Object.keys(data).reduce((acc, key) => {
if (yesNoProperties.has(key) && typeof data[key] === 'boolean') {
Expand Down
56 changes: 21 additions & 35 deletions lib/Classes/Suppressions/SuppressionsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
SuppressionDestroyResult,
SuppressionDestroyResponse
} from '../../Types/Suppressions';
import { APIErrorOptions } from '../../Types/Common';

const createOptions = {
headers: { 'Content-Type': 'application/json' }
Expand Down Expand Up @@ -83,16 +82,13 @@ export default class SuppressionClient
isDataArray: boolean
): Promise<SuppressionCreationResult> {
if (isDataArray) {
throw new APIError({
status: 400,
statusText: 'Data property should be an object',
body: {
message: 'Whitelist\'s creation process does not support multiple creations. Data property should be an object'
}
} as APIErrorOptions);
throw APIError.getUserDataError(
'Data property should be an object',
'Whitelist\'s creation process does not support multiple creations. Data property should be an object'
);
}
return this.request
.postWithFD(urljoin('v3', domain, 'whitelists'), data)
.postWithFD(urljoin('v3', domain, 'whitelists'), data as SuppressionCreationData)
.then(this.prepareResponse);
}

Expand All @@ -103,36 +99,27 @@ export default class SuppressionClient
if (Array.isArray(data)) { // User provided an array
const isContainsTag = data.some((unsubscribe: SuppressionCreationData) => unsubscribe.tag);
if (isContainsTag) {
throw new APIError({
status: 400,
statusText: 'Tag property should not be used for creating multiple unsubscribes.',
body: {
message: 'Tag property can be used only if one unsubscribe provided as second argument of create method. Please use tags instead.'
}
} as APIErrorOptions);
throw APIError.getUserDataError(
'Tag property should not be used for creating multiple unsubscribes.',
'Tag property can be used only if one unsubscribe provided as second argument of create method. Please use tags instead.'
);
}
return this.request
.post(urljoin('v3', domain, 'unsubscribes'), JSON.stringify(data), createOptions)
.then(this.prepareResponse);
}

if (data?.tags) {
throw new APIError({
status: 400,
statusText: 'Tags property should not be used for creating one unsubscribe.',
body: {
message: 'Tags property can be used if you provides an array of unsubscribes as second argument of create method. Please use tag instead'
}
} as APIErrorOptions);
throw APIError.getUserDataError(
'Tags property should not be used for creating one unsubscribe.',
'Tags property can be used if you provides an array of unsubscribes as second argument of create method. Please use tag instead'
);
}
if (Array.isArray(data.tag)) {
throw new APIError({
status: 400,
statusText: 'Tag property can not be an array',
body: {
message: 'Please use array of unsubscribes as second argument of create method to be able to provide few tags'
}
} as APIErrorOptions);
throw APIError.getUserDataError(
'Tag property can not be an array',
'Please use array of unsubscribes as second argument of create method to be able to provide few tags'
);
}
/* We need Form Data for unsubscribes if we want to support the "tag" property */
return this.request
Expand All @@ -144,11 +131,10 @@ export default class SuppressionClient
if (type in this.models) {
return this.models[type as keyof typeof this.models];
}
throw new APIError({
status: 400,
statusText: 'Unknown type value',
body: { message: 'Type may be only one of [bounces, complaints, unsubscribes, whitelists]' }
} as APIErrorOptions);
throw APIError.getUserDataError(
'Unknown type value',
'Type may be only one of [bounces, complaints, unsubscribes, whitelists]'
);
}

private prepareResponse(response: SuppressionCreationResponse): SuppressionCreationResult {
Expand Down
Loading
Loading