Initial code commit
This commit is contained in:
parent
88e75758af
commit
08f2127864
14 changed files with 1633 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
LISTMONK_HOST=
|
||||||
|
LISTMONK_USERNAME=
|
||||||
|
LISTMONK_PASSWORD=
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -128,3 +128,9 @@ dist
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# Private Repository Configuration
|
||||||
|
.npmrc
|
||||||
|
|
||||||
|
# Individualized automation script
|
||||||
|
unpublish-publish.sh
|
||||||
27
README.md
Normal file
27
README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Listmonk Node Client Library
|
||||||
|
The idea of this package is to create a more natural feeling client library (particularly for anybody with [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) experiences) than the existing [Listmonk Node API library](https://github.com/mihairaulea/listmonk-nodejs-api).
|
||||||
|
|
||||||
|
In particular this means abstracting away a lot of the API structure and calls. And trying to make it feel like the way you would give instructions to someone if you were telling them how to work with the software through the web UI.
|
||||||
|
|
||||||
|
That said, still pretty heavy in development and not particularly feature complete. If you need completeness, performance, etc... I'd recommend looking at that other library linked above.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
Granted, this is largely meant as a software package that you would list in your dependencies.
|
||||||
|
But, for demo purposes you'll need to copy and fill out the [`.env.example` file](./.env.example) into a `.env` file.
|
||||||
|
And then run: `yarn demo` (or equivalent). This will create a campaign, template, list and subscriber in the instance you point it to.
|
||||||
|
Under th hood this just runs the "compiled" version of [`demo.ts`](./src/demo.ts)
|
||||||
|
|
||||||
|
[`demo.ts`](./src/demo.ts) is also where I would start if your looking to understand how to use this package. Though it should be fairly straightforward and intuitive for anyone with an OOP and typescript background.
|
||||||
|
|
||||||
|
## Some Notes
|
||||||
|
- Forgetting to remember to call `save()` on objects to sync with Listmonk I suspect will be a popular issue for people
|
||||||
|
- The `fromData` method is an instance method, NOT a static one, so follows a similar theory to ["Hydration"](https://en.wikipedia.org/wiki/Hydration_(web_development)). That is, the instance is usually created and then is filled in with data (these are meant to chain - ex. `new <APIObject>().fromData(...)`)
|
||||||
|
- You could, call constructors but using the static `.create` methods are usually easier/better.
|
||||||
|
- I publish to my own private package repository not NPM (hence the `@BridgemanAccessible/` at the beginning of the package name)
|
||||||
|
|
||||||
|
## Small Promotions
|
||||||
|
- [Azure Communication Services (ACS) SNS Relay - including Listmonk endpoint](https://github.com/AlanBridgeman/acs-sms-relay)
|
||||||
|
- [My Business (Bridgeman Accessible)](https://bridgemanaccessible.ca)
|
||||||
|
|
||||||
|
## Contributing, Issues and Pull Request
|
||||||
|
Feel free to submit issues or pull requests when applicable. I make no promises about answering or any kind of updating/maintenance (particularly on any kind of schedule). But I will try to work with others to have this work for them as I can.
|
||||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "@BridgemanAccessible/listmonk-node-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A Node client library for Listmonk",
|
||||||
|
"repository": "https://github.com/AlanBridgeman/listmonk-nodejs-client.git",
|
||||||
|
"author": "Alan Bridgeman<alan@alanbridgeman.ca>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"demo": "yarn build && node dist/demo.js",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.7.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/API.ts
Normal file
142
src/API.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type defining the credentials to use when authenticating with the Listmonk API.
|
||||||
|
*/
|
||||||
|
export type APICredentials = {
|
||||||
|
host: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that provides an interface to the Listmonk API.
|
||||||
|
*/
|
||||||
|
export class API {
|
||||||
|
/** The hostname of the Listmonk instance. */
|
||||||
|
private host: string;
|
||||||
|
/** The username to use when authenticating with Listmonk. */
|
||||||
|
private username: string;
|
||||||
|
/** The password to use when authenticating with Listmonk. */
|
||||||
|
private password: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Listmonk API instance.
|
||||||
|
*
|
||||||
|
* @param credentials The credentials to use when authenticating with Listmonk.
|
||||||
|
*/
|
||||||
|
constructor(credentials?: APICredentials) {
|
||||||
|
if(typeof credentials === 'undefined') {
|
||||||
|
credentials = this.getDefaultCredentialsFromEnvVars();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.host = credentials.host;
|
||||||
|
this.username = credentials.username;
|
||||||
|
this.password = credentials.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default credentials for the Listmonk API (as found in the environment variables).
|
||||||
|
*
|
||||||
|
* The table below describes the environment variables that are used:
|
||||||
|
*
|
||||||
|
* | Environment Variable | Description |
|
||||||
|
* | -------------------- | ------------------------------------------------------ |
|
||||||
|
* | LISTMONK_HOST | The hostname of the Listmonk instance. |
|
||||||
|
* | LISTMONK_USERNAME | The username to use when authenticating with Listmonk. |
|
||||||
|
* | LISTMONK_PASSWORD | The password to use when authenticating with Listmonk. |
|
||||||
|
*
|
||||||
|
* @returns The default credentials for the Listmonk API (as found in the environment variables).
|
||||||
|
*/
|
||||||
|
private getDefaultCredentialsFromEnvVars(): APICredentials {
|
||||||
|
if(typeof process.env.LISTMONK_HOST === 'undefined' || process.env.LISTMONK_HOST === null || process.env.LISTMONK_HOST === '') {
|
||||||
|
throw new Error('LISTMONK_HOST is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof process.env.LISTMONK_USERNAME === 'undefined' || process.env.LISTMONK_USERNAME === null || process.env.LISTMONK_USERNAME === '') {
|
||||||
|
throw new Error('LISTMONK_USERNAME is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof process.env.LISTMONK_PASSWORD === 'undefined' || process.env.LISTMONK_PASSWORD === null || process.env.LISTMONK_PASSWORD === '') {
|
||||||
|
throw new Error('LISTMONK_PASSWORD is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: process.env.LISTMONK_HOST,
|
||||||
|
username: process.env.LISTMONK_USERNAME,
|
||||||
|
password: process.env.LISTMONK_PASSWORD
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The credentials for the Listmonk API.
|
||||||
|
*/
|
||||||
|
getCredentials(): APICredentials {
|
||||||
|
return {
|
||||||
|
host: this.host,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a GET request to the Listmonk API.
|
||||||
|
*
|
||||||
|
* T (type parameter 1) is the type of the data returned by the API call.
|
||||||
|
*
|
||||||
|
* @param endpoint The endpoint/path to send the GET request to (that is, the URL without the hostname).
|
||||||
|
* @returns A promise that resolves to the results of the API call.
|
||||||
|
*/
|
||||||
|
async get<T = any>(endpoint: string): Promise<T> {
|
||||||
|
const results = await axios.get<{ data: { results: T } }>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, { auth: { username: this.username, password: this.password } })
|
||||||
|
.then(response => { /*console.log(`${endpoint}: ${JSON.stringify(response.data, null, 4)}`);*/ return typeof response.data.data.results !== 'undefined' ? response.data.data.results : (response.data.data as T); });
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a POST request to the Listmonk API.
|
||||||
|
*
|
||||||
|
* T (type parameter 1) is the type of the data returned by the API call.
|
||||||
|
* D (type parameter 2) is the type of the data to send to the API.
|
||||||
|
*
|
||||||
|
* @param endpoint The endpoint/path to send the POST request to (that is, the URL without the hostname).
|
||||||
|
* @param data The data to send to the API.
|
||||||
|
* @returns A promise that resolves to the result of the API call.
|
||||||
|
*/
|
||||||
|
async post<D = any,T = any>(endpoint: string, data: D): Promise<T> {
|
||||||
|
const results = await axios.post<T, AxiosResponse<T,any>,D>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, data, { auth: { username: this.username, password: this.password } }).then(response => response.data);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a PUT request to the Listmonk API.
|
||||||
|
*
|
||||||
|
* T (type parameter 1) is the type of the data returned by the API call.
|
||||||
|
* D (type parameter 2) is the type of the data to send to the API.
|
||||||
|
*
|
||||||
|
* @param endpoint The endpoint/path to send the PUT request to (that is, the URL without the hostname).
|
||||||
|
* @param data The data to send to the API.
|
||||||
|
* @returns A promise that resolves to the result of the API call.
|
||||||
|
*/
|
||||||
|
async put<D = any,T = any>(endpoint: string, data: D): Promise<T> {
|
||||||
|
const results = await axios.put<T, AxiosResponse<T,any>,D>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, data, { auth: { username: this.username, password: this.password } }).then(response => response.data);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a DELETE request to the Listmonk API.
|
||||||
|
*
|
||||||
|
* T (type parameter 1) is the type of the data returned by the API call.
|
||||||
|
*
|
||||||
|
* @param endpoint The endpoint/path to send the DELETE request to (that is, the URL without the hostname).
|
||||||
|
* @returns A promise that resolves to the result of the API call.
|
||||||
|
*/
|
||||||
|
async delete<T = any>(endpoint: string): Promise<T> {
|
||||||
|
const results = await axios.delete<T>(endpoint.startsWith('/') ? this.host + '/api' + endpoint : this.host + '/api/' + endpoint, { auth: { username: this.username, password: this.password } }).then(response => response.data);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/APIObject.ts
Normal file
29
src/APIObject.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { API, APICredentials } from "./API";
|
||||||
|
|
||||||
|
export abstract class APIObject {
|
||||||
|
/** The API instance to use for making requests. */
|
||||||
|
protected api: API;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API object.
|
||||||
|
*
|
||||||
|
* @param api The API instance to use for making requests. Defaults to a new API instance if not provided.
|
||||||
|
*/
|
||||||
|
protected constructor(credentials?: APICredentials) {
|
||||||
|
this.api = new API(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the object to a JSON object.
|
||||||
|
*
|
||||||
|
* This is mostly used for API call. But also sometimes for code reuse purposes.
|
||||||
|
*/
|
||||||
|
abstract toJSON(): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the object to be created from data.
|
||||||
|
*
|
||||||
|
* @param data The data to create the object from.
|
||||||
|
*/
|
||||||
|
abstract fromData<T>(data: T): APIObject | Promise<APIObject>;
|
||||||
|
}
|
||||||
529
src/Campaign.ts
Normal file
529
src/Campaign.ts
Normal file
|
|
@ -0,0 +1,529 @@
|
||||||
|
import { APIObject } from './APIObject';
|
||||||
|
import { API, APICredentials } from './API';
|
||||||
|
import { List, ListData } from './List';
|
||||||
|
import { Template } from './Template';
|
||||||
|
|
||||||
|
type CampaignData = {
|
||||||
|
id?: number,
|
||||||
|
uuid?: string,
|
||||||
|
name: string,
|
||||||
|
subject: string,
|
||||||
|
lists: number[] | ListData[],
|
||||||
|
type: "regular" | "optin",
|
||||||
|
content_type: 'richtext' | 'html' | 'markdown' | 'plain',
|
||||||
|
body: string,
|
||||||
|
from_email?: string,
|
||||||
|
alt_body?: string,
|
||||||
|
send_at?: string,
|
||||||
|
messenger?: string,
|
||||||
|
template_id?: number,
|
||||||
|
tags?: string[],
|
||||||
|
headers?: { [key: string]: string },
|
||||||
|
status?: "draft" | "scheduled" | "running" | "paused" | "cancelled",
|
||||||
|
created_at?: string,
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a campaign in Listmonk.
|
||||||
|
*/
|
||||||
|
export class Campaign extends APIObject {
|
||||||
|
/** Identifier for the Campaign */
|
||||||
|
private id?: number;
|
||||||
|
/** Universal Unique Identifier (UUID) for the Campaign */
|
||||||
|
private uuid?: string;
|
||||||
|
/** Campaign name. */
|
||||||
|
private name: string;
|
||||||
|
/** Campaign email subject. */
|
||||||
|
private subject: string;
|
||||||
|
/** Lists to send campaign to. */
|
||||||
|
private lists: List[];
|
||||||
|
/** Campaign type: 'regular' or 'optin'. */
|
||||||
|
private type?: 'regular' | 'optin';
|
||||||
|
/** Content type: `richtext`, `html`, `markdown`, `plain` */
|
||||||
|
private contentType?: 'richtext' | 'html' | 'markdown' | 'plain';
|
||||||
|
/** Content body of campaign. */
|
||||||
|
private campaignBody: string;
|
||||||
|
/** 'From' email in campaign emails. Defaults to value from settings if not provided. */
|
||||||
|
private fromEmail?: string;
|
||||||
|
/** Alternate plain text body for HTML (and richtext) emails. */
|
||||||
|
private altBody?: string;
|
||||||
|
/** Timestamp to schedule campaign. Format: `YYYY-MM-DDTHH:MM:SSZ` */
|
||||||
|
private sendAt?: string;
|
||||||
|
/** `email` or a custom messenger defined in settings. Defaults to `email` if not provided. */
|
||||||
|
private messenger?: string;
|
||||||
|
/** Template to use */
|
||||||
|
private template?: Template;
|
||||||
|
/** Tags to mark campaign. */
|
||||||
|
private tags?: string[];
|
||||||
|
/** Key-value pairs to send as SMTP headers. Example: `[{"x-custom-header": "value"}]`. */
|
||||||
|
private headers?: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* status for campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`.
|
||||||
|
*
|
||||||
|
* Note:
|
||||||
|
* - Only `scheduled` campaigns can change status to `draft`.
|
||||||
|
* - Only `draft` campaigns can change status to `scheduled`.
|
||||||
|
* - Only `paused` and `draft` campaigns can start (`running` status).
|
||||||
|
* - Only `running` campaigns can change status to `cancelled` and `paused`.
|
||||||
|
*/
|
||||||
|
private status?: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled';
|
||||||
|
/** Timestamp for when the campaign was created. Format: `YYYY-MM-DDTHH:MM:SSZ` */
|
||||||
|
private createdAt?: string;
|
||||||
|
/** Timestamp for when the campaign was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ` */
|
||||||
|
private updatedAt?: string;
|
||||||
|
|
||||||
|
constructor(credentials?: APICredentials, id?: number, uuid?: string, name?: string, subject?: string, lists?: List[], type?: 'regular' | 'optin', contentType?: 'richtext' | 'html' | 'markdown' | 'plain', campaignBody?: string, fromEmail?: string, altBody?: string, sendAt?: string, messenger?: string, template?: Template, tags?: string[], headers?: { [key: string]: string }, status?: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled', createdAt?: string, updatedAt?: string) {
|
||||||
|
super(credentials);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = typeof name !== 'undefined' ? name : '';
|
||||||
|
this.subject = typeof subject !== 'undefined' ? subject : '';
|
||||||
|
this.lists = typeof lists !== 'undefined' ? lists : [];
|
||||||
|
this.type = type;
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.campaignBody = typeof campaignBody !== 'undefined' ? campaignBody : '';
|
||||||
|
this.fromEmail = fromEmail;
|
||||||
|
this.altBody = altBody;
|
||||||
|
this.sendAt = sendAt;
|
||||||
|
this.messenger = messenger;
|
||||||
|
this.template = template;
|
||||||
|
this.tags = typeof tags !== 'undefined' ? tags : [];
|
||||||
|
this.headers = headers;
|
||||||
|
this.status = status;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Identifier for the Campaign
|
||||||
|
*/
|
||||||
|
getID(): number | undefined {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Universal Unique Identifier (UUID) for the Campaign
|
||||||
|
*/
|
||||||
|
getUUID(): string | undefined {
|
||||||
|
return this.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Campaign name
|
||||||
|
*/
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the name of the campaign
|
||||||
|
*
|
||||||
|
* @param name The name to set the Campaign name to
|
||||||
|
*/
|
||||||
|
setName(name: string): void {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Campaign email subject
|
||||||
|
*/
|
||||||
|
getSubject(): string {
|
||||||
|
return this.subject;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the subject of the campaign
|
||||||
|
*
|
||||||
|
* @param subject The subject to set the Campaign email subject to
|
||||||
|
*/
|
||||||
|
setSubject(subject: string): void {
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Lists to send the campaign to
|
||||||
|
*/
|
||||||
|
getLists(): List[] {
|
||||||
|
return this.lists;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a list for the campaign to send to
|
||||||
|
*
|
||||||
|
* @param list List to add to the campaign
|
||||||
|
*/
|
||||||
|
addList(list: List): void {
|
||||||
|
this.lists.push(list);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove a list from the campaign
|
||||||
|
*
|
||||||
|
* @param list List to remove from the campaign
|
||||||
|
*/
|
||||||
|
removeList(list: List): void {
|
||||||
|
this.lists = this.lists.filter((l: List) => l.getID() !== list.getID());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check if the campaign includes a list
|
||||||
|
*
|
||||||
|
* @param list The list to check if the campaign includes
|
||||||
|
* @returns A boolean indicating if the campaign includes the list
|
||||||
|
*/
|
||||||
|
includesList(list: List): boolean {
|
||||||
|
return this.lists.some((l: List) => l.getID() === list.getID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Campaign type: 'regular' or 'optin'
|
||||||
|
*/
|
||||||
|
getType(): 'regular' | 'optin' | undefined {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the type of the campaign
|
||||||
|
*
|
||||||
|
* @param type Campaign type: 'regular' or 'optin'
|
||||||
|
*/
|
||||||
|
setType(type: 'regular' | 'optin'): void {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Content type: `richtext`, `html`, `markdown`, `plain`
|
||||||
|
*/
|
||||||
|
getContentType(): 'richtext' | 'html' | 'markdown' | 'plain' | undefined {
|
||||||
|
return this.contentType;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the content type of the campaign
|
||||||
|
*
|
||||||
|
* @param contentType Content type: `richtext`, `html`, `markdown`, `plain`
|
||||||
|
*/
|
||||||
|
setContentType(contentType: 'richtext' | 'html' | 'markdown' | 'plain'): void {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Content body of the campaign
|
||||||
|
*/
|
||||||
|
getCampaignBody(): string {
|
||||||
|
return this.campaignBody;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the content body of the campaign
|
||||||
|
*
|
||||||
|
* @param campaignBody The content body to set the campaign body to
|
||||||
|
*/
|
||||||
|
setCampaignBody(campaignBody: string): void {
|
||||||
|
this.campaignBody = campaignBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns 'From' email in campaign emails. Defaults to value from settings if not provided.
|
||||||
|
*/
|
||||||
|
getFromEmail(): string | undefined {
|
||||||
|
return this.fromEmail;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the 'From' email in campaign emails
|
||||||
|
*
|
||||||
|
* @param fromEmail The email to set the 'From' email in campaign emails to
|
||||||
|
*/
|
||||||
|
setFromEmail(fromEmail: string): void {
|
||||||
|
this.fromEmail = fromEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Alternate plain text body for HTML (and richtext) emails
|
||||||
|
*/
|
||||||
|
getAltBody(): string | undefined {
|
||||||
|
return this.altBody;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the alternate plain text body for HTML (and richtext) emails
|
||||||
|
*
|
||||||
|
* @param altBody The alternate plain text body to set for HTML (and richtext) emails
|
||||||
|
*/
|
||||||
|
setAltBody(altBody: string): void {
|
||||||
|
this.altBody = altBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Timestamp to schedule the campaign. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
getSendAt(): string | undefined {
|
||||||
|
return this.sendAt;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the timestamp to schedule the campaign
|
||||||
|
*
|
||||||
|
* @param sendAt The timestamp to set to schedule the campaign
|
||||||
|
*/
|
||||||
|
setSendAt(sendAt: string): void {
|
||||||
|
this.sendAt = sendAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns `email` or a custom messenger defined in settings. Defaults to `email` if not provided.
|
||||||
|
*/
|
||||||
|
getMessenger(): string | undefined {
|
||||||
|
return this.messenger;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the messenger for the campaign
|
||||||
|
*
|
||||||
|
* @param messenger The messenger to set for the campaign
|
||||||
|
*/
|
||||||
|
setMessenger(messenger: string): void {
|
||||||
|
this.messenger = messenger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the template used for the campaign
|
||||||
|
*
|
||||||
|
* @returns ID of the template used for the campaign or -1 if the template isn't set explicitly
|
||||||
|
*/
|
||||||
|
getTemplateID(): number {
|
||||||
|
return this.getTemplate()?.getID() ?? -1;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns The template used for the campaign
|
||||||
|
*/
|
||||||
|
getTemplate(): Template | undefined {
|
||||||
|
return this.template;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the template for the campaign
|
||||||
|
*
|
||||||
|
* @param template The template to set for the campaign
|
||||||
|
*/
|
||||||
|
setTemplate(template: Template): void {
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the template for the campaign by it's ID
|
||||||
|
*
|
||||||
|
* @param templateID The ID of the template to set for the campaign
|
||||||
|
*/
|
||||||
|
setTemplateByID(templateID: number): void {
|
||||||
|
this.template = new Template(this.api.getCredentials(), templateID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Tags to mark the campaign
|
||||||
|
*/
|
||||||
|
getTags(): string[] | undefined {
|
||||||
|
return this.tags;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a tag to the campaign
|
||||||
|
*
|
||||||
|
* @param tag The tag to add to the campaign
|
||||||
|
*/
|
||||||
|
addTag(tag: string): void {
|
||||||
|
if(this.tags === undefined) {
|
||||||
|
this.tags = [];
|
||||||
|
}
|
||||||
|
this.tags.push(tag);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove a tag from the campaign
|
||||||
|
*
|
||||||
|
* @param tag The tag to remove from the campaign
|
||||||
|
*/
|
||||||
|
removeTag(tag: string): void {
|
||||||
|
if(this.tags === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tags = this.tags.filter((t: string) => t !== tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Key-value pairs to send as SMTP headers
|
||||||
|
*/
|
||||||
|
getHeaders(): { [key: string]: string } | undefined {
|
||||||
|
return this.headers;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a header to the campaign
|
||||||
|
*
|
||||||
|
* @param key The key of the header to add
|
||||||
|
* @param value The value of the header to add
|
||||||
|
*/
|
||||||
|
addHeader(key: string, value: string): void {
|
||||||
|
if(this.headers === undefined) {
|
||||||
|
this.headers = {};
|
||||||
|
}
|
||||||
|
this.headers[key] = value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Remove a header from the campaign
|
||||||
|
*
|
||||||
|
* @param key The key of the header to remove
|
||||||
|
*/
|
||||||
|
removeHeader(key: string): void {
|
||||||
|
if(this.headers === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this.headers[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Status for the campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`
|
||||||
|
*/
|
||||||
|
getStatus(): 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled' | undefined {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the status for the campaign
|
||||||
|
*
|
||||||
|
* Note:
|
||||||
|
* - Only `scheduled` campaigns can change status to `draft`.
|
||||||
|
* - Only `draft` campaigns can change status to `scheduled`.
|
||||||
|
* - Only `paused` and `draft` campaigns can start (`running` status).
|
||||||
|
* - Only `running` campaigns can change status to `cancelled` and `paused`.
|
||||||
|
*
|
||||||
|
* @param status The status to set for the campaign: `draft`, `scheduled`, `running`, `paused`, `cancelled`
|
||||||
|
*/
|
||||||
|
setStatus(status: 'draft' | 'scheduled' | 'running' | 'paused' | 'cancelled'): void {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Timestamp for when the campaign was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
getCreatedAt(): string | undefined {
|
||||||
|
return this.createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Timestamp for when the campaign was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
getUpdatedAt(): string | undefined {
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates the campaign in Listmonk.
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
const data = this.toJSON();
|
||||||
|
|
||||||
|
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
|
||||||
|
if(typeof data.uuid === 'undefined') {
|
||||||
|
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
|
||||||
|
delete data.id;
|
||||||
|
delete data.uuid;
|
||||||
|
delete data.created_at;
|
||||||
|
delete data.updated_at;
|
||||||
|
delete data.status;
|
||||||
|
|
||||||
|
|
||||||
|
// Make the API call to create the subscriber in Listmonk
|
||||||
|
await this.api.post<CampaignData>('/campaigns', data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.api.put<CampaignData>('/campaigns/' + data.id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): CampaignData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
uuid: this.uuid,
|
||||||
|
name: this.name,
|
||||||
|
subject: this.subject,
|
||||||
|
lists: this.lists.map((list: List) => list.getID() as number),
|
||||||
|
type: typeof this.type !== 'undefined' ? this.type : 'regular',
|
||||||
|
content_type: typeof this.contentType !== 'undefined' ? this.contentType : 'plain',
|
||||||
|
body: this.campaignBody,
|
||||||
|
from_email: this.fromEmail,
|
||||||
|
alt_body: this.altBody,
|
||||||
|
send_at: this.sendAt,
|
||||||
|
messenger: this.messenger,
|
||||||
|
template_id: this.template?.getID(),
|
||||||
|
tags: this.tags,
|
||||||
|
headers: this.headers,
|
||||||
|
status: typeof this.status !== 'undefined' ? this.status : 'draft',
|
||||||
|
created_at: this.createdAt,
|
||||||
|
updated_at: this.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fromData<CampaignData>(data: CampaignData): Promise<Campaign> {
|
||||||
|
const typedData = data as {id?: number, uuid?: string, name: string, subject: string, lists: number[] | ListData[], type: "regular" | "optin", content_type: 'richtext' | 'html' | 'markdown' | 'plain', body: string, from_email?: string, alt_body?: string, send_at?: string, messenger?: string, template_id?: number, tags?: string[], headers?: { [key: string]: string }, status: "draft" | "scheduled" | "running" | "paused" | "cancelled", created_at?: string, updated_at?: string };
|
||||||
|
const lists = (
|
||||||
|
await Promise.all(
|
||||||
|
typedData.lists.map(
|
||||||
|
async (listData: Number | ListData) => {
|
||||||
|
if(typeof listData === 'number') {
|
||||||
|
return await List.find((list: List) => list.getID() === listData);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new List().fromData(listData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).filter((list: List | undefined) => typeof list !== 'undefined');
|
||||||
|
|
||||||
|
const template = typeof typedData.template_id !== 'undefined' ? await Template.find((template: Template) => template.getID() === typedData.template_id) : undefined;
|
||||||
|
|
||||||
|
this.id = typedData.id;
|
||||||
|
this.uuid = typedData.uuid;
|
||||||
|
this.setName(typedData.name);
|
||||||
|
this.setSubject(typedData.subject);
|
||||||
|
this.lists = lists;
|
||||||
|
this.setType(typedData.type);
|
||||||
|
this.setContentType(typedData.content_type);
|
||||||
|
this.setCampaignBody(typedData.body);
|
||||||
|
this.fromEmail = typedData.from_email;
|
||||||
|
this.altBody = typedData.alt_body;
|
||||||
|
this.sendAt = typedData.send_at;
|
||||||
|
this.messenger = typedData.messenger;
|
||||||
|
this.template = template;
|
||||||
|
this.tags = typedData.tags;
|
||||||
|
this.headers = typedData.headers;
|
||||||
|
this.setStatus(typedData.status);
|
||||||
|
this.createdAt = typedData.created_at;
|
||||||
|
this.updatedAt = typedData.updated_at;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Campaign object with the given data.
|
||||||
|
*
|
||||||
|
* Note, this doesn't automatically create the campaign in Listmonk. You need to call `save` to do that.
|
||||||
|
*
|
||||||
|
* @param name Name of the new campaign.
|
||||||
|
* @param subject Subject of the new campaign.
|
||||||
|
* @param lists Lists to send the campaign to.
|
||||||
|
* @param type Type of the campaign: 'regular' or 'optin'.
|
||||||
|
* @param contentType Content type: `richtext`, `html`, `markdown`, `plain`
|
||||||
|
* @param body Raw HTML body of the campaign
|
||||||
|
* @returns The new campaign object
|
||||||
|
*/
|
||||||
|
static async create(name: string, subject: string, lists: List[], type: "regular" | "optin", content_type: 'richtext' | 'html' | 'markdown' | 'plain', body: string, credentials?: APICredentials): Promise<Campaign> {
|
||||||
|
return await new Campaign(credentials).fromData({ name, subject, lists: lists.map((list: List) => list.toJSON()), type, content_type, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a campaign based on the given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A function that takes a campaign and returns true if it matches the desired template.
|
||||||
|
* @param credentials Optional credentials to use to authenticate API requests
|
||||||
|
* @returns The campaign that matches the predicate, or undefined if no campaign matches the predicate.
|
||||||
|
*/
|
||||||
|
static async find(predicate: (campaign: Campaign) => boolean, credentials?: APICredentials): Promise<Campaign | undefined> {
|
||||||
|
return await new API(credentials).get<CampaignData[]>('/campaigns')
|
||||||
|
.then(async (campaigns: CampaignData[]) => {
|
||||||
|
// Convert each CampaignData JSON object to a Campaign object
|
||||||
|
const campaignObjs: Campaign[] = await Promise.all(campaigns.map((campaign: CampaignData) => new Campaign(credentials).fromData(campaign)));
|
||||||
|
|
||||||
|
// Use the predicate function provided to find the desired campaign
|
||||||
|
const campaign = campaignObjs.find(predicate);
|
||||||
|
|
||||||
|
return campaign;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/List.ts
Normal file
131
src/List.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { APIObject } from "./APIObject";
|
||||||
|
import { API, APICredentials } from "./API";
|
||||||
|
|
||||||
|
export type ListData = {
|
||||||
|
id?: number,
|
||||||
|
uuid?: string,
|
||||||
|
name: string,
|
||||||
|
type: 'public' | 'private',
|
||||||
|
optin?: 'single' | 'double',
|
||||||
|
tags: string[],
|
||||||
|
created_at?: string,
|
||||||
|
updated_at?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export class List extends APIObject {
|
||||||
|
private id?: number;
|
||||||
|
private uuid?: string;
|
||||||
|
private name: string;
|
||||||
|
private type?: 'public' | 'private';
|
||||||
|
private optin?: 'single' | 'double';
|
||||||
|
private tags: string[];
|
||||||
|
private createdAt?: string;
|
||||||
|
private updatedAt?: string;
|
||||||
|
|
||||||
|
constructor(credentials?: APICredentials, id?: number, uuid?: string, name?: string, type?: 'public' | 'private', optin?: 'single' | 'double', tags?: string[], createdAt?: string, updatedAt?: string) {
|
||||||
|
super(credentials);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = typeof name !== 'undefined' ? name : '';
|
||||||
|
this.type = type;
|
||||||
|
this.optin = optin;
|
||||||
|
this.tags = typeof tags !== 'undefined' ? tags : [];
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getID(): number | undefined {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates the list in Listmonk.
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
const data = this.toJSON();
|
||||||
|
|
||||||
|
// If the UUID is not set, then we assume we need to create the list in Listmonk
|
||||||
|
if(typeof data.uuid === 'undefined') {
|
||||||
|
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
|
||||||
|
delete data.id;
|
||||||
|
delete data.created_at;
|
||||||
|
delete data.updated_at;
|
||||||
|
delete data.uuid;
|
||||||
|
|
||||||
|
// Make the API call to create the list in Listmonk
|
||||||
|
await this.api.post<ListData>('/lists', data);
|
||||||
|
} else {
|
||||||
|
// Make the API call to update the list in Listmonk
|
||||||
|
await this.api.post<ListData>('/lists/' + data.uuid, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ListData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
uuid: this.uuid,
|
||||||
|
name: this.name,
|
||||||
|
type: typeof this.type !== 'undefined' ? this.type : 'private',
|
||||||
|
optin: this.optin,
|
||||||
|
tags: this.tags,
|
||||||
|
created_at: this.createdAt,
|
||||||
|
updated_at: this.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fromData<ListData>(data: ListData): List {
|
||||||
|
const typedData = data as { id?: number, uuid?: string, name: string, type: 'public' | 'private', optin?: 'single' | 'double', tags: string[], created_at?: string, updated_at?: string };
|
||||||
|
|
||||||
|
this.id = typedData.id;
|
||||||
|
this.uuid = typedData.uuid;
|
||||||
|
this.name = typedData.name;
|
||||||
|
this.type = typedData.type;
|
||||||
|
this.optin = typedData.optin;
|
||||||
|
this.tags = typedData.tags;
|
||||||
|
this.createdAt = typedData.created_at;
|
||||||
|
this.updatedAt = typedData.updated_at;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new List object with the given data.
|
||||||
|
*
|
||||||
|
* Note, this doesn't automatically create the list in Listmonk. You need to call `save` to do that.
|
||||||
|
*
|
||||||
|
* @param name Name of the new list.
|
||||||
|
* @param type Type of list. Options: private, public.
|
||||||
|
* @param optin Opt-in type. Options: single, double.
|
||||||
|
* @param tags Associated tags for a list.
|
||||||
|
* @param credentials Optional credentials to use for the API call.
|
||||||
|
* @returns A new List object with the given data.
|
||||||
|
*/
|
||||||
|
static async create(name: string, type: 'public' | 'private', optin: 'single' | 'double', tags?: string[], credentials?: APICredentials): Promise<List> {
|
||||||
|
return await new List(credentials).fromData({ name, type, optin, tags });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a list based on a predicate function.
|
||||||
|
*
|
||||||
|
* @param predicate The function to use to find the desired list.
|
||||||
|
* @param credentials Optional The credentials to use for the API call.
|
||||||
|
* @returns The list that matches the predicate, or undefined if no list matches the predicate.
|
||||||
|
*/
|
||||||
|
static async find(predicate: (list: List) => boolean, credentials?: APICredentials): Promise<List | undefined> {
|
||||||
|
return await new API(credentials).get<ListData[]>('/lists')
|
||||||
|
.then((lists: ListData[]) => {
|
||||||
|
// Convert each LIstData JSOn object to a List object
|
||||||
|
const listObjs: List[] = lists.map((list: ListData) => new List(credentials).fromData(list));
|
||||||
|
|
||||||
|
// Use the predicate function provided to find the desired list
|
||||||
|
const list = listObjs.find(predicate);
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/ListMembership.ts
Normal file
47
src/ListMembership.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { APIObject } from './APIObject';
|
||||||
|
import { APICredentials } from './API';
|
||||||
|
import { List, ListData } from './List';
|
||||||
|
|
||||||
|
export type ListMembershipData = {
|
||||||
|
subscription_status: 'unconfirmed' | 'confirmed',
|
||||||
|
[key: string]: any
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ListMembership extends APIObject {
|
||||||
|
private subscriptionStatus?: 'unconfirmed' | 'confirmed';
|
||||||
|
private list?: List;
|
||||||
|
|
||||||
|
constructor(credentials?: APICredentials, subscriptionStatus?: 'unconfirmed' | 'confirmed', list?: List) {
|
||||||
|
super(credentials);
|
||||||
|
|
||||||
|
this.subscriptionStatus = subscriptionStatus;
|
||||||
|
this.list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscriptionStatus(): 'unconfirmed' | 'confirmed' | undefined {
|
||||||
|
return this.subscriptionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
getList(): List | undefined {
|
||||||
|
return this.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ListMembershipData {
|
||||||
|
return {
|
||||||
|
subscription_status: typeof this.subscriptionStatus !== 'undefined' ? this.subscriptionStatus : 'unconfirmed',
|
||||||
|
...this.list?.toJSON()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fromData<ListMembershipData>(data: ListMembershipData): ListMembership {
|
||||||
|
// NOTE types are weird here because TypeScript was complaining otherwise
|
||||||
|
const subscriptionStatus = (data as { subscription_status: 'unconfirmed' | 'confirmed', [key: string]: any }).subscription_status;
|
||||||
|
delete (data as { subscription_status?: 'unconfirmed' | 'confirmed', [key: string]: any }).subscription_status;
|
||||||
|
const list = new List().fromData(data as ListData);
|
||||||
|
|
||||||
|
this.subscriptionStatus = subscriptionStatus;
|
||||||
|
this.list = list;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/Subscriber.ts
Normal file
216
src/Subscriber.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import { APIObject } from "./APIObject";
|
||||||
|
import { API, APICredentials } from "./API";
|
||||||
|
import { List } from "./List";
|
||||||
|
import { ListMembership, ListMembershipData } from "./ListMembership";
|
||||||
|
|
||||||
|
type SubscriberData = {
|
||||||
|
id?: number,
|
||||||
|
uuid?: string,
|
||||||
|
email: string,
|
||||||
|
name: string,
|
||||||
|
attribs: {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
status: "enabled" | "blocklisted",
|
||||||
|
lists: number[] | ListMembershipData[],
|
||||||
|
created_at?: string,
|
||||||
|
updated_at?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Subscriber extends APIObject {
|
||||||
|
private id?: number;
|
||||||
|
private uuid?: string;
|
||||||
|
private email: string;
|
||||||
|
private name: string;
|
||||||
|
private attribs: { [key: string]: any };
|
||||||
|
private status?: 'enabled' | 'blocklisted';
|
||||||
|
private lists: ListMembership[];
|
||||||
|
private createdAt?: string;
|
||||||
|
private updatedAt?: string;
|
||||||
|
|
||||||
|
constructor(credentials?: APICredentials, id?: number, uuid?: string, email?: string, name?: string, attribs?: { [key: string]: any }, status?: 'enabled' | 'blocklisted', lists?: ListMembership[], createdAt?: string, updatedAt?: string) {
|
||||||
|
super(credentials);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.email = typeof email !== 'undefined' ? email : '';
|
||||||
|
this.name = typeof name !== 'undefined' ? name : '';
|
||||||
|
this.attribs = typeof attribs !== 'undefined' ? attribs : {};
|
||||||
|
this.status = status;
|
||||||
|
this.lists = typeof lists !== 'undefined' ? lists : [];
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getID(): number | undefined {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUUID(): string | undefined {
|
||||||
|
return this.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmail(): string {
|
||||||
|
return this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttributes(): { [key: string]: any } {
|
||||||
|
return this.attribs;
|
||||||
|
}
|
||||||
|
hasAttribute(key: string): boolean {
|
||||||
|
return typeof this.attribs[key] !== 'undefined';
|
||||||
|
}
|
||||||
|
getAttribute(key: string): any {
|
||||||
|
return this.hasAttribute(key) ? this.attribs[key] : undefined;
|
||||||
|
}
|
||||||
|
setAttribute(key: string, value: any) {
|
||||||
|
this.attribs[key] = value;
|
||||||
|
}
|
||||||
|
removeAttribute(key: string) {
|
||||||
|
delete this.attribs[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): 'enabled' | 'blocklisted' | undefined {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLists(): ListMembership[] {
|
||||||
|
return this.lists;
|
||||||
|
}
|
||||||
|
addList(list: List) {
|
||||||
|
this.lists.push(new ListMembership(this.api.getCredentials()).fromData({ subscription_status: 'unconfirmed', ...list.toJSON() }));
|
||||||
|
}
|
||||||
|
removeList(list: ListMembership) {
|
||||||
|
this.lists = this.lists.filter((listMembership: ListMembership) => listMembership !== list);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreatedAt(): string | undefined {
|
||||||
|
return this.createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdatedAt(): string | undefined {
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates the subscriber in Listmonk.
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
const data = this.toJSON();
|
||||||
|
|
||||||
|
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
|
||||||
|
if(typeof data.uuid === 'undefined') {
|
||||||
|
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
|
||||||
|
delete data.id;
|
||||||
|
delete data.created_at;
|
||||||
|
delete data.updated_at;
|
||||||
|
delete data.uuid;
|
||||||
|
|
||||||
|
// Make the API call to create the subscriber in Listmonk
|
||||||
|
await this.api.post<SubscriberData>('/subscribers', data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.api.put<SubscriberData>('/subscribers/' + data.id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): SubscriberData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
created_at: this.createdAt,
|
||||||
|
updated_at: this.updatedAt,
|
||||||
|
uuid: this.uuid,
|
||||||
|
email: this.email,
|
||||||
|
name: this.name,
|
||||||
|
attribs: this.attribs,
|
||||||
|
status: this.status as 'enabled' | 'blocklisted',
|
||||||
|
lists: this.lists.map((listMembership: ListMembership) => (listMembership.getList() as List).getID() as number)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fromData<SubscriberData>(data: SubscriberData): Promise<Subscriber> {
|
||||||
|
// NOTE types are weird here because TypeScript was complaining otherwise
|
||||||
|
const typedData = (data as { id?: number, uuid?: string, email: string, name: string, attribs: { [key: string]: any }, status: "enabled" | "blocklisted", lists: number[] | ListMembershipData[], created_at?: string, updated_at?: string });
|
||||||
|
|
||||||
|
let listMemberships: ListMembership[] = [];
|
||||||
|
// We only want to bother processing the lists if there are any
|
||||||
|
if(typedData.lists.length > 0) {
|
||||||
|
listMemberships = await Promise.all(
|
||||||
|
typedData.lists.map(
|
||||||
|
async (listMembershipData: number | ListMembershipData) => {
|
||||||
|
if(typeof listMembershipData === 'number') {
|
||||||
|
// Get the list object based on it's IDs from Listmonk
|
||||||
|
const listObj = await List.find((list: List) => (list.getID() as number) === listMembershipData);
|
||||||
|
|
||||||
|
// Verify we found a list with the given ID
|
||||||
|
if(typeof listObj === 'undefined') {
|
||||||
|
throw new Error('List with ID ' + listMembershipData + ' not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default the membership object to unconfirmed
|
||||||
|
listMembershipData = {
|
||||||
|
subscription_status: 'unconfirmed',
|
||||||
|
...listObj.toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ListMembership(this.api.getCredentials()).fromData(listMembershipData);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.id = typedData.id;
|
||||||
|
this.uuid = typedData.uuid;
|
||||||
|
this.email = typedData.email;
|
||||||
|
this.name = typedData.name;
|
||||||
|
this.attribs = typedData.attribs;
|
||||||
|
this.status = typedData.status;
|
||||||
|
this.lists = listMemberships;
|
||||||
|
this.createdAt = typedData.created_at;
|
||||||
|
this.updatedAt = typedData.updated_at;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Subscriber object with the given data.
|
||||||
|
*
|
||||||
|
* Note, this doesn't automatically create the subscriber in Listmonk. You need to call `save` to do that.
|
||||||
|
*
|
||||||
|
* @param email Subscriber's email address |
|
||||||
|
* @param name Subscriber's name. |
|
||||||
|
* @param status Subscriber's status. |
|
||||||
|
* @param lists List of list IDs to subscribe to. |
|
||||||
|
* @param attribs Attributes of the new subscriber. |
|
||||||
|
* @param preconfirm_subscriptions If true, subscriptions are marked as confirmed and no-optin emails are sent for double opt-in lists. |
|
||||||
|
* @param credentials Optional The credentials to use for the API call.
|
||||||
|
*/
|
||||||
|
static async create(email: string, name: string, status: 'enabled' | 'blocklisted', lists?: List[], attribs?: { [key: string]: any }, preconfirm_subscriptions?: boolean, credentials?: APICredentials): Promise<Subscriber> {
|
||||||
|
return await new Subscriber(credentials).fromData({ email, name, status, lists: typeof lists !== 'undefined' ? lists.map((list: List) => list.getID()) : [] , attribs });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a subscriber based on the given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A function that takes a subscriber and returns true if it matches the desired subscriber.
|
||||||
|
* @param credentials Optional The credentials to use for the API call.
|
||||||
|
* @returns The subscriber that matches the predicate, or undefined if no subscriber matches the predicate.
|
||||||
|
*/
|
||||||
|
static async find(predicate: (subscriber: Subscriber) => boolean, credentials?: APICredentials): Promise<Subscriber | undefined> {
|
||||||
|
return await new API(credentials).get<SubscriberData[]>('/subscribers?per_page=all')
|
||||||
|
.then(async (subscribers: SubscriberData[]) => {
|
||||||
|
// Convert each SubscriberData JSON object to a Subscriber object
|
||||||
|
const subscriberObjs: Subscriber[] = await Promise.all(subscribers.map((subscriber: SubscriberData) => new Subscriber(credentials).fromData(subscriber)));
|
||||||
|
|
||||||
|
// Use the predicate function provided to find the desired list
|
||||||
|
const subscriber = subscriberObjs.find(predicate);
|
||||||
|
|
||||||
|
return subscriber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
238
src/Template.ts
Normal file
238
src/Template.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
import { APIObject } from './APIObject';
|
||||||
|
import { API, APICredentials } from './API';
|
||||||
|
|
||||||
|
type TemplateData = {
|
||||||
|
id?: number,
|
||||||
|
name: string,
|
||||||
|
type: 'campaign' | 'tx',
|
||||||
|
body: string,
|
||||||
|
subject?: string,
|
||||||
|
is_default?: boolean,
|
||||||
|
created_at?: string,
|
||||||
|
updated_at?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a template in Listmonk.
|
||||||
|
*/
|
||||||
|
export class Template extends APIObject {
|
||||||
|
private id?: number;
|
||||||
|
/** Name of the template */
|
||||||
|
private name: string;
|
||||||
|
/** Type of the template (`campaign` or `tx`) */
|
||||||
|
private type?: 'campaign' | 'tx';
|
||||||
|
/** Raw HTML body of the template */
|
||||||
|
private body: string;
|
||||||
|
/** Subject line for the template (only for tx) */
|
||||||
|
private subject?: string;
|
||||||
|
/** Whether the template is the default template */
|
||||||
|
private isDefault?: boolean;
|
||||||
|
/** Timestamp when the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ` */
|
||||||
|
private createdAt?: string;
|
||||||
|
/** Timestamp when the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ` */
|
||||||
|
private updatedAt?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new template object
|
||||||
|
*
|
||||||
|
* @param credentials Optional credentials to use to authenticate API requests
|
||||||
|
* @param id The identifier for the template
|
||||||
|
* @param name The name of the template
|
||||||
|
* @param type The type of the template (`campaign` or `tx`)
|
||||||
|
* @param body The raw HTML body of the template
|
||||||
|
* @param isDefault Whether the template is the default template
|
||||||
|
* @param createdAt When the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
* @param updatedAt When the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
constructor(credentials?: APICredentials, id?: number, name?: string, type?: "campaign" | "tx", body?: string, isDefault?: boolean, createdAt?: string, updatedAt?: string) {
|
||||||
|
super(credentials);
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.name = typeof name !== 'undefined' ? name : '';
|
||||||
|
this.body = typeof body !== 'undefined' ? body : '';
|
||||||
|
this.type = type;
|
||||||
|
this.isDefault = isDefault;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Identifier for the template
|
||||||
|
*/
|
||||||
|
getID(): number | undefined {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The name of the template
|
||||||
|
*/
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the name of the template
|
||||||
|
*
|
||||||
|
* @param name Name of the template
|
||||||
|
*/
|
||||||
|
setName(name: string): void {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Raw HTML body of the template
|
||||||
|
*/
|
||||||
|
getBody(): string {
|
||||||
|
return this.body;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the raw HTML body of the template
|
||||||
|
*
|
||||||
|
* @param body Raw HTML body of the template
|
||||||
|
*/
|
||||||
|
setBody(body: string): void {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Type of the template (`campaign` or `tx`)
|
||||||
|
*/
|
||||||
|
getType(): 'campaign' | 'tx' | undefined {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the type of the template
|
||||||
|
*
|
||||||
|
* @param type Type of the template (`campaign` or `tx`)
|
||||||
|
*/
|
||||||
|
setType(type: 'campaign' | 'tx'): void {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Subject line for the template (only for tx)
|
||||||
|
*/
|
||||||
|
getSubject(): string | undefined {
|
||||||
|
return this.subject;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the subject line for the template
|
||||||
|
*
|
||||||
|
* NOTE: ONLY relevant for `tx` type templates
|
||||||
|
*
|
||||||
|
* @param subject Subject line for the template
|
||||||
|
*/
|
||||||
|
setSubject(subject: string): void {
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Whether the template is the default template
|
||||||
|
*/
|
||||||
|
getIsDefault(): boolean | undefined {
|
||||||
|
return this.isDefault;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set whether the template is the default template
|
||||||
|
*
|
||||||
|
* @param isDefault Whether the template is the default template
|
||||||
|
*/
|
||||||
|
setIsDefault(isDefault: boolean): void {
|
||||||
|
this.isDefault = isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Timestamp when the template was created. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
getCreatedAt(): string | undefined {
|
||||||
|
return this.createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Timestamp when the template was last updated. Format: `YYYY-MM-DDTHH:MM:SSZ`
|
||||||
|
*/
|
||||||
|
getUpdatedAt(): string | undefined {
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates the template in Listmonk.
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
const data = this.toJSON();
|
||||||
|
|
||||||
|
// If the UUID is not set, then we assume we need to create the subscriber in Listmonk
|
||||||
|
if(typeof data.id === 'undefined') {
|
||||||
|
// Technically, this might be unnecessary since the values should be set to undefined anyway, but it's good to be safe
|
||||||
|
delete data.id;
|
||||||
|
delete data.created_at;
|
||||||
|
delete data.updated_at;
|
||||||
|
|
||||||
|
// Make the API call to create the subscriber in Listmonk
|
||||||
|
await this.api.post<TemplateData>('/templates', data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.api.put<TemplateData>('/templates/' + data.id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): TemplateData {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
body: this.body,
|
||||||
|
type: typeof this.type !== 'undefined' ? this.type : 'campaign',
|
||||||
|
is_default: this.isDefault,
|
||||||
|
created_at: this.createdAt,
|
||||||
|
updated_at: this.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fromData<TemplateData>(data: TemplateData): Template {
|
||||||
|
const typedData = data as { id?: number, name: string, type: 'campaign' | 'tx', body: string, subject?: string, is_default?: boolean, created_at?: string, updated_at?: string};
|
||||||
|
|
||||||
|
this.id = typedData.id;
|
||||||
|
this.name = typedData.name
|
||||||
|
this.type = typedData.type;
|
||||||
|
this.body = typedData.body;
|
||||||
|
this.isDefault = typedData.is_default;
|
||||||
|
this.createdAt = typedData.created_at;
|
||||||
|
this.updatedAt = typedData.updated_at;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Template object with the given data.
|
||||||
|
*
|
||||||
|
* Note, this doesn't automatically create the template in Listmonk. You need to call `save` to do that.
|
||||||
|
*
|
||||||
|
* @param name Name of the new template.
|
||||||
|
* @param type Type of the template (`campaign` or `tx`)
|
||||||
|
* @param body Raw HTML body of the template
|
||||||
|
* @param credentials Optional credentials to use to authenticate API requests
|
||||||
|
* @returns The new template object
|
||||||
|
*/
|
||||||
|
static async create(name: string, type: 'campaign' | 'tx', body: string, credentials?: APICredentials): Promise<Template> {
|
||||||
|
return await new Template(credentials).fromData({ name, type, body });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a template based on the given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A function that takes a template and returns true if it matches the desired template.
|
||||||
|
* @param credentials Optional credentials to use to authenticate API requests
|
||||||
|
* @returns The template that matches the predicate, or undefined if no template matches the predicate.
|
||||||
|
*/
|
||||||
|
static async find(predicate: (template: Template) => boolean, credentials?: APICredentials): Promise<Template | undefined> {
|
||||||
|
return await new API(credentials).get<TemplateData[]>('/templates')
|
||||||
|
.then(async (templates: TemplateData[]) => {
|
||||||
|
// Convert each TemplateData JSON object to a Template object
|
||||||
|
const templateObjs: Template[] = await Promise.all(templates.map((template: TemplateData) => new Template(credentials).fromData(template)));
|
||||||
|
|
||||||
|
// Use the predicate function provided to find the desired template
|
||||||
|
const template = templateObjs.find(predicate);
|
||||||
|
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/demo.ts
Normal file
53
src/demo.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
import { List } from './List';
|
||||||
|
import { Subscriber } from './Subscriber';
|
||||||
|
import { Template } from './Template';
|
||||||
|
import { Campaign } from './Campaign';
|
||||||
|
|
||||||
|
// Load environment variables from .env file (mostly Listmonki API credentials)
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Create a new list
|
||||||
|
await (await List.create('API Generated List', 'private', 'single')).save();
|
||||||
|
|
||||||
|
// Get a list by it's name
|
||||||
|
const list = await List.find((list: List) => list.getName() === 'API Generated List');
|
||||||
|
if(typeof list === 'undefined') {
|
||||||
|
throw new Error('List not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subscriber
|
||||||
|
await (await Subscriber.create('api@nodejs.listmonk', 'Listmonk Node Client Library', 'enabled', [/*list*/], { phone: '+12345678912' })).save();
|
||||||
|
|
||||||
|
// Get a subscriber by their email
|
||||||
|
const subscriber = await Subscriber.find((subscriber: Subscriber) => subscriber.getEmail() === 'api@nodejs.listmonk');
|
||||||
|
if(typeof subscriber === 'undefined') {
|
||||||
|
throw new Error('Subscriber not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the subscriber to the list
|
||||||
|
subscriber.addList(list);
|
||||||
|
await subscriber.save();
|
||||||
|
|
||||||
|
// Create a template
|
||||||
|
await (await Template.create('API Generated Template', 'campaign', 'Hello {{ .Subscriber.FirstName }},<br>{{ template "content" . }}<br>Your Phone number on record is: {{ .Subscriber.Attribs.phone }}')).save();
|
||||||
|
|
||||||
|
// Get a template by it's name
|
||||||
|
const template = await Template.find((template: Template) => template.getName() === 'API Generated Template');
|
||||||
|
if(typeof template === 'undefined') {
|
||||||
|
throw new Error('Template not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a campaign
|
||||||
|
const campaign = (await Campaign.create('API Generated Campaign', 'API Generated Campaign Subject', [list], 'regular', 'html', '<h1>API Generated Text</h1><br><p>A demo campaign generated using the Listmonk Node Client Library</p>'));
|
||||||
|
|
||||||
|
// Set the template for the campaign
|
||||||
|
campaign.setTemplate(template);
|
||||||
|
|
||||||
|
// Save the campaign to Listmonk
|
||||||
|
await campaign.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
110
tsconfig.json
Normal file
110
tsconfig.json
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
82
yarn.lock
Normal file
82
yarn.lock
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@^22.7.5":
|
||||||
|
version "22.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b"
|
||||||
|
integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~6.19.2"
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
axios@^1.7.7:
|
||||||
|
version "1.7.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
|
||||||
|
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.6"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
dotenv@^16.4.5:
|
||||||
|
version "16.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||||
|
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||||
|
|
||||||
|
follow-redirects@^1.15.6:
|
||||||
|
version "1.15.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||||
|
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||||
|
|
||||||
|
form-data@^4.0.0:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||||
|
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
typescript@^5.6.3:
|
||||||
|
version "5.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
|
||||||
|
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
|
||||||
|
|
||||||
|
undici-types@~6.19.2:
|
||||||
|
version "6.19.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||||
|
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||||
Loading…
Add table
Add a link
Reference in a new issue