Add node SDK (#19)

This commit is contained in:
KM Koushik
2024-05-23 22:02:33 +10:00
committed by GitHub
parent e0fc68d4c0
commit 5fb2448e07
11 changed files with 1295 additions and 2337 deletions

View File

@@ -49,6 +49,7 @@
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"tldts": "^6.1.16",
"unsend": "workspace:*",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -1,4 +1,7 @@
import { env } from "~/env";
import { Unsend } from "unsend";
const unsend = new Unsend(env.UNSEND_API_KEY);
export async function sendSignUpEmail(
email: string,
@@ -26,29 +29,22 @@ async function sendMail(
html: string
) {
if (env.UNSEND_API_KEY && env.UNSEND_URL) {
const resp = await fetch(`${env.UNSEND_URL}/emails`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.UNSEND_API_KEY}`,
},
body: JSON.stringify({
from: "no-reply@auth.unsend.dev",
to: email,
subject,
text,
html,
}),
const resp = await unsend.emails.send({
to: email,
from: "no-reply@auth.unsend.dev",
subject,
text,
html,
});
if (resp.status === 200) {
if (resp.data) {
console.log("Email sent using unsend");
return;
} else {
console.log(
"Error sending email using unsend, so fallback to resend",
resp.status,
resp.statusText
resp.error?.code,
resp.error?.message
);
}
} else {

10
packages/sdk/.eslintrc.js Normal file
View 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
View File

@@ -0,0 +1 @@
export { Unsend } from "./src/unsend";

27
packages/sdk/package.json Normal file
View 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
View 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
View 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);
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@unsend/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,4 @@
export type ErrorResponse = {
message: string;
code: string;
};

134
packages/sdk/types/schema.d.ts vendored Normal file
View 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

File diff suppressed because it is too large Load Diff