Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/sdk-tags-settings-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sendlit": minor
---

Add `tags` (list, references, rename, remove), `settings.general` (get, update), `overview.get`, and `sequences.sendEmailTest`, so platforms can manage tags, set the mailing address, show dashboard numbers and send test emails without calling the API by hand.
5 changes: 4 additions & 1 deletion packages/sendlit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,15 @@ new SendLit({
| `sendlit.contacts` | `create`, `list`, `listAll`, `get`, `update`, `remove`, `addTag`, `removeTag`, `deliveries`, `events` |
| `sendlit.segments` | `create`, `list`, `get`, `update`, `remove` |
| `sendlit.templates` | `list`, `listSystem`, `get`, `create`, `update`, `duplicate`, `remove` |
| `sendlit.sequences` | Broadcasts and sequences: `create`, `list`, `listAll`, `get`, `update`, `remove`, `addEmail`, `updateEmail`, `publishChanges`, `discardChanges`, `emailVersions`, `restoreEmailVersion`, `removeEmail`, `start`, `pause`, `stats`, `subscribers` |
| `sendlit.sequences` | Broadcasts and sequences: `create`, `list`, `listAll`, `get`, `update`, `remove`, `addEmail`, `updateEmail`, `publishChanges`, `discardChanges`, `emailVersions`, `restoreEmailVersion`, `removeEmail`, `start`, `pause`, `stats`, `subscribers`, `sendEmailTest` |
| `sendlit.emails` | Transactional email: `send`, `get`, `list`, `listAll` |
| `sendlit.events` | Custom events: `track`, and `definitions.list`, `definitions.get`, `definitions.create`, `definitions.update`, `definitions.delete` |
| `sendlit.provisioning` | Organization key only: `provisionTeam`, `getTeam`, `updateTeam`, `createTeamKey`, `suspendTeam`, `resumeTeam`, `archiveTeam`, `getTeamUsage` |
| `sendlit.suppressions` | `list`, `listAll`, `get`, `release` |
| `sendlit.deliveryEvents` | `list`, `listAll`, `get` |
| `sendlit.tags` | `list`, `references`, `rename`, `remove` |
| `sendlit.settings` | `general.get`, `general.update` (the mailing address broadcasts and sequences need) |
| `sendlit.overview` | `get`: the team's dashboard numbers, optionally for `rangeDays` 1, 3, 7 or 30 |

Request and response types are exported too, for example `Contact`,
`CreateContactInput`, `Sequence`, and `SendEmailInput`.
Expand Down
12 changes: 12 additions & 0 deletions packages/sendlit/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { Contacts } from "./resources/contacts";
import { DeliveryEvents } from "./resources/delivery-events";
import { Emails } from "./resources/emails";
import { Events } from "./resources/events";
import { OverviewResource } from "./resources/overview";
import { Provisioning } from "./resources/provisioning";
import { Segments } from "./resources/segments";
import { Sequences } from "./resources/sequences";
import { Settings } from "./resources/settings";
import { Suppressions } from "./resources/suppressions";
import { Tags } from "./resources/tags";
import { Templates } from "./resources/templates";
import { assertId } from "./validate";

Expand Down Expand Up @@ -62,6 +65,12 @@ export class SendLit {
readonly provisioning: Provisioning;
readonly suppressions: Suppressions;
readonly deliveryEvents: DeliveryEvents;
/** Tags across the team's contacts: list, rename and delete them. */
readonly tags: Tags;
/** Team settings, such as the mailing address. */
readonly settings: Settings;
/** The team's dashboard numbers. */
readonly overview: OverviewResource;

constructor(options: SendLitOptions = {}) {
const g = globalThis as { window?: { document?: unknown } };
Expand Down Expand Up @@ -121,5 +130,8 @@ export class SendLit {
this.provisioning = new Provisioning(http);
this.suppressions = new Suppressions(http);
this.deliveryEvents = new DeliveryEvents(http);
this.tags = new Tags(http);
this.settings = new Settings(http);
this.overview = new OverviewResource(http);
}
}
3 changes: 3 additions & 0 deletions packages/sendlit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ export * from "./resources/contacts";
export * from "./resources/delivery-events";
export * from "./resources/emails";
export * from "./resources/events";
export * from "./resources/overview";
export * from "./resources/provisioning";
export * from "./resources/segments";
export * from "./resources/sequences";
export * from "./resources/settings";
export * from "./resources/suppressions";
export * from "./resources/tags";
export * from "./resources/templates";
37 changes: 37 additions & 0 deletions packages/sendlit/src/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,43 @@ const cases: Case[] = [
query: "?page=1&limit=20",
},
],
[
"sequences.sendEmailTest",
(s) => s.sequences.sendEmailTest("seq_1", "eml_1", { to: ["ada@example.com"] }),
{
method: "POST",
path: "/sequences/seq_1/emails/eml_1/test",
body: { to: ["ada@example.com"] },
},
],
["tags.list", (s) => s.tags.list(), { method: "GET", path: "/tags" }],
[
"tags.references",
(s) => s.tags.references("early bird"),
{ method: "GET", path: "/tags/early%20bird/references" },
],
[
"tags.rename",
(s) => s.tags.rename("vip", { name: "VIP" }),
{ method: "PATCH", path: "/tags/vip", body: { name: "VIP" } },
],
["tags.remove", (s) => s.tags.remove("vip"), { method: "DELETE", path: "/tags/vip" }],
[
"settings.general.get",
(s) => s.settings.general.get(),
{ method: "GET", path: "/settings/general" },
],
[
"settings.general.update",
(s) => s.settings.general.update({ mailingAddress: "1 Main St" }),
{ method: "PUT", path: "/settings/general", body: { mailingAddress: "1 Main St" } },
],
["overview.get", (s) => s.overview.get(), { method: "GET", path: "/overview" }],
[
"overview.get with range",
(s) => s.overview.get({ rangeDays: 30 }),
{ method: "GET", path: "/overview", query: "?rangeDays=30" },
],
[
"emails.send",
(s) =>
Expand Down
17 changes: 17 additions & 0 deletions packages/sendlit/src/resources/overview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { contract } from "@sendlit/api-contract";
import type { HttpClient, Ok, Query } from "../http";

const routes = contract.overview;

/** The team's dashboard numbers: contacts, sends, opens and clicks over a
* period. */
export type Overview = Ok<typeof routes.get>;
export type OverviewQuery = Query<typeof routes.get>;

export class OverviewResource {
constructor(private readonly http: HttpClient) {}

async get(query: OverviewQuery = {}): Promise<Overview> {
return this.http.request(routes.get, { query });
}
}
19 changes: 19 additions & 0 deletions packages/sendlit/src/resources/sequences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type DiscardSequenceChangesInput = Body<typeof routes.discardChanges>;
export type SequenceEmailVersion = Ok<typeof routes.emailVersions>["items"][number];
export type ListSequencesQuery = Query<typeof routes.list>;
export type ListSequenceSubscribersQuery = Query<typeof routes.subscribers>;
export type SendSequenceEmailTestInput = Body<typeof routes.sendEmailTest>;
export type SequenceEmailTestResult = Ok<typeof routes.sendEmailTest>;

export class Sequences {
constructor(private readonly http: HttpClient) {}
Expand Down Expand Up @@ -158,6 +160,23 @@ export class Sequences {
});
}

/** Sends a test of one broadcast or sequence email, rendered like a real
* send with a `[Test]` subject prefix. Tests count against the team's test
* limits (429 `test_send_limit_reached` / `test_send_email_limit_reached`). */
async sendEmailTest(
sequenceId: string,
emailId: string,
input: SendSequenceEmailTestInput,
): Promise<SequenceEmailTestResult> {
return this.http.request(routes.sendEmailTest, {
params: {
sequenceId: assertId(sequenceId, "sequenceId"),
emailId: assertId(emailId, "emailId"),
},
body: input,
});
}

/** IDs of contacts that have received this broadcast or sequence. */
async subscribers(
sequenceId: string,
Expand Down
29 changes: 29 additions & 0 deletions packages/sendlit/src/resources/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { contract } from "@sendlit/api-contract";
import type { Body, HttpClient, Ok } from "../http";

const routes = contract.settings.general;

/** The team's general settings, such as the mailing address that broadcasts
* and sequences need before they can send. */
export type GeneralSettings = Ok<typeof routes.get>;
export type UpdateGeneralSettingsInput = Body<typeof routes.update>;

export class GeneralSettingsResource {
constructor(private readonly http: HttpClient) {}

async get(): Promise<GeneralSettings> {
return this.http.request(routes.get);
}

async update(input: UpdateGeneralSettingsInput): Promise<GeneralSettings> {
return this.http.request(routes.update, { body: input });
}
}

export class Settings {
readonly general: GeneralSettingsResource;

constructor(http: HttpClient) {
this.general = new GeneralSettingsResource(http);
}
}
43 changes: 43 additions & 0 deletions packages/sendlit/src/resources/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { contract } from "@sendlit/api-contract";
import type { Body, HttpClient, Ok } from "../http";
import { encodeSegment } from "../validate";

const routes = contract.tags;

/** A tag in use in the team, with how many contacts carry it. */
export type Tag = Ok<typeof routes.list>["items"][number];
/** Where a tag is still used (segments, sequence triggers, ...), so it can be
* shown before the tag is renamed or deleted. */
export type TagReferences = Ok<typeof routes.references>;
export type RenameTagInput = Body<typeof routes.rename>;
export type RenameTagResult = Ok<typeof routes.rename>;
export type RemoveTagResult = Ok<typeof routes.remove>;

export class Tags {
constructor(private readonly http: HttpClient) {}

async list(): Promise<Ok<typeof routes.list>> {
return this.http.request(routes.list);
}

async references(tag: string): Promise<TagReferences> {
return this.http.request(routes.references, {
params: { tag: encodeSegment(tag, "tag") },
});
}

/** Renames a tag on every contact and everywhere it's referenced. */
async rename(tag: string, input: RenameTagInput): Promise<RenameTagResult> {
return this.http.request(routes.rename, {
params: { tag: encodeSegment(tag, "tag") },
body: input,
});
}

/** Removes a tag from every contact. */
async remove(tag: string): Promise<RemoveTagResult> {
return this.http.request(routes.remove, {
params: { tag: encodeSegment(tag, "tag") },
});
}
}
Loading