Add node SDK (#19)
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"tldts": "^6.1.16",
|
"tldts": "^6.1.16",
|
||||||
|
"unsend": "workspace:*",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import { Unsend } from "unsend";
|
||||||
|
|
||||||
|
const unsend = new Unsend(env.UNSEND_API_KEY);
|
||||||
|
|
||||||
export async function sendSignUpEmail(
|
export async function sendSignUpEmail(
|
||||||
email: string,
|
email: string,
|
||||||
@@ -26,29 +29,22 @@ async function sendMail(
|
|||||||
html: string
|
html: string
|
||||||
) {
|
) {
|
||||||
if (env.UNSEND_API_KEY && env.UNSEND_URL) {
|
if (env.UNSEND_API_KEY && env.UNSEND_URL) {
|
||||||
const resp = await fetch(`${env.UNSEND_URL}/emails`, {
|
const resp = await unsend.emails.send({
|
||||||
method: "POST",
|
to: email,
|
||||||
headers: {
|
from: "no-reply@auth.unsend.dev",
|
||||||
"Content-Type": "application/json",
|
subject,
|
||||||
Authorization: `Bearer ${env.UNSEND_API_KEY}`,
|
text,
|
||||||
},
|
html,
|
||||||
body: JSON.stringify({
|
|
||||||
from: "no-reply@auth.unsend.dev",
|
|
||||||
to: email,
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.data) {
|
||||||
console.log("Email sent using unsend");
|
console.log("Email sent using unsend");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"Error sending email using unsend, so fallback to resend",
|
"Error sending email using unsend, so fallback to resend",
|
||||||
resp.status,
|
resp.error?.code,
|
||||||
resp.statusText
|
resp.error?.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
10
packages/sdk/.eslintrc.js
Normal file
10
packages/sdk/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["@unsend/eslint-config/library.js"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.json",
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
};
|
1
packages/sdk/index.ts
Normal file
1
packages/sdk/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Unsend } from "./src/unsend";
|
27
packages/sdk/package.json
Normal file
27
packages/sdk/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "unsend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint . --max-warnings 0",
|
||||||
|
"build": "rm -rf dist && tsup index.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.12.12",
|
||||||
|
"@types/react": "^18.3.2",
|
||||||
|
"@unsend/eslint-config": "workspace:*",
|
||||||
|
"@unsend/typescript-config": "workspace:*",
|
||||||
|
"openapi-typescript": "^6.7.6",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-email/render": "^0.0.14",
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
}
|
57
packages/sdk/src/email.ts
Normal file
57
packages/sdk/src/email.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { renderAsync } from "@react-email/render";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Unsend } from "./unsend";
|
||||||
|
import { paths } from "../types/schema";
|
||||||
|
import { ErrorResponse } from "../types";
|
||||||
|
|
||||||
|
type SendEmailPayload =
|
||||||
|
paths["/v1/emails"]["post"]["requestBody"]["content"]["application/json"] & {
|
||||||
|
react?: React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateEmailResponse = {
|
||||||
|
data: CreateEmailResponseSuccess | null;
|
||||||
|
error: ErrorResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateEmailResponseSuccess =
|
||||||
|
paths["/v1/emails"]["post"]["responses"]["200"]["content"]["application/json"];
|
||||||
|
|
||||||
|
type GetEmailResponseSuccess =
|
||||||
|
paths["/v1/emails/{emailId}"]["get"]["responses"]["200"]["content"]["application/json"];
|
||||||
|
|
||||||
|
type GetEmailResponse = {
|
||||||
|
data: GetEmailResponseSuccess | null;
|
||||||
|
error: ErrorResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Emails {
|
||||||
|
constructor(private readonly unsend: Unsend) {
|
||||||
|
this.unsend = unsend;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(payload: SendEmailPayload) {
|
||||||
|
return this.create(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(payload: SendEmailPayload): Promise<CreateEmailResponse> {
|
||||||
|
if (payload.react) {
|
||||||
|
payload.html = await renderAsync(payload.react as React.ReactElement);
|
||||||
|
delete payload.react;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.unsend.post<CreateEmailResponseSuccess>(
|
||||||
|
"/emails",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<GetEmailResponse> {
|
||||||
|
const data = await this.unsend.get<GetEmailResponseSuccess>(
|
||||||
|
`/emails/${id}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
119
packages/sdk/src/unsend.ts
Normal file
119
packages/sdk/src/unsend.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { ErrorResponse } from "../types";
|
||||||
|
import { Emails } from "./email";
|
||||||
|
|
||||||
|
const defaultBaseUrl = "https://app.unsend.dev";
|
||||||
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||||
|
const baseUrl = `${process?.env?.UNSEND_BASE_URL ?? defaultBaseUrl}/api/v1`;
|
||||||
|
|
||||||
|
function isUNSENDErrorResponse(error: { error: ErrorResponse }) {
|
||||||
|
return error.error.code !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Unsend {
|
||||||
|
private readonly headers: Headers;
|
||||||
|
|
||||||
|
// readonly domains = new Domains(this);
|
||||||
|
readonly emails = new Emails(this);
|
||||||
|
|
||||||
|
constructor(readonly key?: string) {
|
||||||
|
if (!key) {
|
||||||
|
if (typeof process !== "undefined" && process.env) {
|
||||||
|
this.key = process.env.UNSEND_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error(
|
||||||
|
'Missing API key. Pass it to the constructor `new Unsend("re_123")`'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headers = new Headers({
|
||||||
|
Authorization: `Bearer ${this.key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchRequest<T>(
|
||||||
|
path: string,
|
||||||
|
options = {}
|
||||||
|
): Promise<{ data: T | null; error: ErrorResponse | null }> {
|
||||||
|
const response = await fetch(`${baseUrl}${path}`, options);
|
||||||
|
const defaultError = {
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: response.statusText,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
try {
|
||||||
|
const resp = await response.json();
|
||||||
|
if (isUNSENDErrorResponse(resp)) {
|
||||||
|
return { data: null, error: resp };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: null, error: resp.error };
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: defaultError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: null, error: defaultError };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { data, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(path: string, body: unknown) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.fetchRequest<T>(path, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(path: string) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.fetchRequest<T>(path, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put<T>(path: string, body: any) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.fetchRequest<T>(path, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch<T>(path: string, body: any) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.fetchRequest<T>(path, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete<T>(path: string, body?: unknown) {
|
||||||
|
const requestOptions = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: this.headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.fetchRequest<T>(path, requestOptions);
|
||||||
|
}
|
||||||
|
}
|
8
packages/sdk/tsconfig.json
Normal file
8
packages/sdk/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@unsend/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
4
packages/sdk/types/index.ts
Normal file
4
packages/sdk/types/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type ErrorResponse = {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
};
|
134
packages/sdk/types/schema.d.ts
vendored
Normal file
134
packages/sdk/types/schema.d.ts
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* This file was auto-generated by openapi-typescript.
|
||||||
|
* Do not make direct changes to the file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export interface paths {
|
||||||
|
"/v1/domains": {
|
||||||
|
get: {
|
||||||
|
responses: {
|
||||||
|
/** @description Retrieve the user */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": ({
|
||||||
|
/**
|
||||||
|
* @description The ID of the domain
|
||||||
|
* @example 1
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* @description The name of the domain
|
||||||
|
* @example example.com
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @description The ID of the team
|
||||||
|
* @example 1
|
||||||
|
*/
|
||||||
|
teamId: number;
|
||||||
|
/** @enum {string} */
|
||||||
|
status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE";
|
||||||
|
/** @default us-east-1 */
|
||||||
|
region?: string;
|
||||||
|
/** @default false */
|
||||||
|
clickTracking?: boolean;
|
||||||
|
/** @default false */
|
||||||
|
openTracking?: boolean;
|
||||||
|
publicKey: string;
|
||||||
|
dkimStatus?: string | null;
|
||||||
|
spfDetails?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
})[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/v1/emails/{emailId}": {
|
||||||
|
get: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
emailId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Retrieve the user */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
id: string;
|
||||||
|
teamId: number;
|
||||||
|
to: string;
|
||||||
|
from: string;
|
||||||
|
subject: string;
|
||||||
|
html: string | null;
|
||||||
|
text: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
emailEvents: ({
|
||||||
|
emailId: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
status: "QUEUED" | "SENT" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "DELIVERED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAYED";
|
||||||
|
createdAt: string;
|
||||||
|
data?: unknown;
|
||||||
|
})[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"/v1/emails": {
|
||||||
|
post: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
/** Format: email */
|
||||||
|
to: string;
|
||||||
|
/** Format: email */
|
||||||
|
from: string;
|
||||||
|
subject: string;
|
||||||
|
replyTo?: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: {
|
||||||
|
filename: string;
|
||||||
|
content: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Retrieve the user */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
emailId?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type webhooks = Record<string, never>;
|
||||||
|
|
||||||
|
export interface components {
|
||||||
|
schemas: {
|
||||||
|
};
|
||||||
|
responses: never;
|
||||||
|
parameters: {
|
||||||
|
};
|
||||||
|
requestBodies: never;
|
||||||
|
headers: never;
|
||||||
|
pathItems: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type $defs = Record<string, never>;
|
||||||
|
|
||||||
|
export type external = Record<string, never>;
|
||||||
|
|
||||||
|
export type operations = Record<string, never>;
|
3243
pnpm-lock.yaml
generated
3243
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user