Add node SDK (#19)
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"tldts": "^6.1.16",
|
||||
"unsend": "workspace:*",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -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
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