diff --git a/packages/common/src/newsletters-scheduler/api/index.ts b/packages/common/src/newsletters-scheduler/api/index.ts new file mode 100644 index 00000000000..e37df41e3fc --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/index.ts @@ -0,0 +1,2 @@ +export * from "./subscriptions"; +export * from "./types"; diff --git a/packages/common/src/newsletters-scheduler/api/orval/api/orval-newsletters-scheduler.ts b/packages/common/src/newsletters-scheduler/api/orval/api/orval-newsletters-scheduler.ts new file mode 100644 index 00000000000..51de79f0831 --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/orval/api/orval-newsletters-scheduler.ts @@ -0,0 +1,105 @@ +/* tslint:disable:interface-over-type-literal */ +import { Awaited } from "../awaited-type"; + +/** + * Generated by orval v6.24.0 🍺 + * Do not edit manually. + * Hub Newsletters Scheduler + * OpenAPI spec version: 0.0.1 + */ +import { customClient } from "../custom-client"; +export type ISubscriptionMetadata = { [key: string]: any }; + +export type ISubscriptionCatalog = { [key: string]: any } | null; + +export interface IUser { + agoId: string; + createdAt: string; + deleted: boolean; + email: string; + firstName: string; + lastName: string; + optedOut: boolean; + updatedAt: string; + username: string; +} + +export interface INotificationSpec { + createdAt: string; + createdById: string; + description: string; + id: number; + name: string; + updatedAt: string; +} + +export enum DeliveryMethod { + EMAIL = "EMAIL", +} +export interface ISubscription { + actions: SubscriptionActions[]; + active: boolean; + cadence: Cadence; + catalog?: ISubscriptionCatalog; + createdAt: string; + deliveryMethod: DeliveryMethod; + entityId: string; + entityType: SubscriptionEntityType; + id: number; + lastDelivery: string; + metadata: ISubscriptionMetadata; + notificationSpec?: INotificationSpec; + notificationSpecId: number; + updatedAt: string; + user?: IUser; + userId: string; +} + +export enum SystemNotificationSpecNames { + TELEMETRY_REPORT = "TELEMETRY_REPORT", + EVENT = "EVENT", + DISCUSSION_ON_ENTITY = "DISCUSSION_ON_ENTITY", +} +export enum SubscriptionEntityType { + DISCUSSION = "DISCUSSION", +} +export enum Cadence { + ON_EVENT = "ON_EVENT", + DAILY = "DAILY", + WEEKLY = "WEEKLY", + MONTHLY = "MONTHLY", +} +export enum SubscriptionActions { + DISCUSSION_POST_PENDING = "DISCUSSION_POST_PENDING", +} +export interface INotify { + /** An array of actions representing user selections that further customize the subscription behavior */ + actions?: SubscriptionActions[]; + /** Frequency of the subscription */ + cadence: Cadence; + /** The AGO id of the entity associated with the subscription */ + entityId?: string; + /** The type of entity associated with the subscription entityId */ + entityType?: SubscriptionEntityType; + /** Notification spec name for the subscription */ + notificationSpecName: SystemNotificationSpecNames; +} + +type SecondParameter any> = Parameters[1]; + +export const notify = ( + iNotify: INotify, + options?: SecondParameter +) => { + return customClient( + { + url: `/api/newsletters-scheduler/v1/subscriptions/notify`, + method: "POST", + headers: { "Content-Type": "application/json" }, + data: iNotify, + }, + options + ); +}; + +export type NotifyResult = NonNullable>>; diff --git a/packages/common/src/newsletters-scheduler/api/orval/awaited-type.ts b/packages/common/src/newsletters-scheduler/api/orval/awaited-type.ts new file mode 100644 index 00000000000..af18d30b7cb --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/orval/awaited-type.ts @@ -0,0 +1,5 @@ +/** + * Orval generates return types for functions using utility type Awaited + * This was introduced in Typescript 4.5, but hub.js is using Typescript 3 + */ +export type Awaited = T extends PromiseLike ? U : T; diff --git a/packages/common/src/newsletters-scheduler/api/orval/custom-client.ts b/packages/common/src/newsletters-scheduler/api/orval/custom-client.ts new file mode 100644 index 00000000000..04a8bee5bfa --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/orval/custom-client.ts @@ -0,0 +1,94 @@ +/** + * Generated and copied from the [hub engagement repo](https://github.com/ArcGIS/hub-newsletters/blob/master/orval/custom-client.ts) + * Do not edit manually + */ +export interface IOrvalParams { + url: string; + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + headers?: HeadersInit; + params?: Record; // query params + data?: Record; // request body +} + +export interface ICustomParams { + hubApiUrl?: string; + token?: string; + headers?: HeadersInit; + params?: Record; // query params + data?: Record; // request body + mode?: RequestMode; + cache?: RequestCache; + credentials?: RequestCredentials; +} + +export async function customClient( + orvalParams: IOrvalParams, + customParams: ICustomParams +): Promise { + const { url, method, data } = orvalParams; + const { mode, cache, credentials } = customParams; + const { headers, params } = combineParams(orvalParams, customParams); + + const baseUrl = removeTrailingSlash(customParams.hubApiUrl); + const requestUrl = `${baseUrl}${url}?${new URLSearchParams(params)}`; + + const requestOptions: RequestInit = { + headers, + method, + cache, + credentials, + mode, + }; + if (data) { + requestOptions.body = JSON.stringify(data); + } + + const res = await fetch(requestUrl, requestOptions); + const { statusText, status } = res; + + if (res.ok) { + return res.json(); + } + + const error = await res.json(); + throw new RemoteServerError( + statusText, + requestUrl, + status, + JSON.stringify(error.message) + ); +} + +function removeTrailingSlash(hubApiUrl = "https://hub.arcgis.com") { + return hubApiUrl.replace(/\/$/, ""); +} + +function combineParams(orvalParams: IOrvalParams, options: ICustomParams) { + const headers = new Headers({ + ...orvalParams.headers, + ...options.headers, + }); + if (options.token) { + headers.set("Authorization", options.token); + } + + const params = { + ...orvalParams.params, + ...options.params, + }; + + return { headers, params }; +} + +class RemoteServerError extends Error { + status: number; + url: string; + error: string; + + constructor(message: string, url: string, status: number, error: string) { + super(message); + this.status = status; + this.url = url; + this.error = error; + } +} diff --git a/packages/common/src/newsletters-scheduler/api/subscriptions.ts b/packages/common/src/newsletters-scheduler/api/subscriptions.ts new file mode 100644 index 00000000000..c7b42fc21ca --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/subscriptions.ts @@ -0,0 +1,17 @@ +import { INotifyParams } from "./types"; +import { authenticateRequest } from "./utils/authenticate-request"; +import { + notify as _notify, + ISubscription, +} from "./orval/api/orval-newsletters-scheduler"; + +/** + * Notify (schedule) subscriptions to recipients + * + * @param {INotifyParams} options + * @return {Promise} + */ +export async function notify(options: INotifyParams): Promise { + options.token = await authenticateRequest(options); + return _notify(options.data, options); +} diff --git a/packages/common/src/newsletters-scheduler/api/types.ts b/packages/common/src/newsletters-scheduler/api/types.ts new file mode 100644 index 00000000000..23fdf014e99 --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/types.ts @@ -0,0 +1,37 @@ +export { + ISubscriptionMetadata, + ISubscriptionCatalog, + IUser, + INotificationSpec, + DeliveryMethod, + ISubscription, + SystemNotificationSpecNames, + SubscriptionEntityType, + Cadence, + SubscriptionActions, + INotify, +} from "./orval/api/orval-newsletters-scheduler"; +import { IHubRequestOptions } from "../../types"; +import { INotify } from "./orval/api/orval-newsletters-scheduler"; + +/** + * options for making requests against the Newsletters Scheduler API + * + * @export + * @interface INewslettersSchedulerRequestOptions + * @extends IHubRequestOptions + */ +export interface INewslettersSchedulerRequestOptions + extends Omit, + Pick { + httpMethod?: "GET" | "POST" | "PATCH" | "DELETE"; + isPortal?: boolean; + token?: string; + data?: { + [key: string]: any; + }; +} + +export interface INotifyParams extends INewslettersSchedulerRequestOptions { + data: INotify; +} diff --git a/packages/common/src/newsletters-scheduler/api/utils/authenticate-request.ts b/packages/common/src/newsletters-scheduler/api/utils/authenticate-request.ts new file mode 100644 index 00000000000..301f66075b8 --- /dev/null +++ b/packages/common/src/newsletters-scheduler/api/utils/authenticate-request.ts @@ -0,0 +1,20 @@ +import { INewslettersSchedulerRequestOptions } from "../types"; + +/** + * return a token created using options.authentication or set on options.token + * + * @export + * @param {INewslettersSchedulerRequestOptions} options + * @return {*} {Promise} + */ +export function authenticateRequest( + options: INewslettersSchedulerRequestOptions +): Promise { + const { token, authentication } = options; + + if (authentication) { + return authentication.getToken(authentication.portal); + } + + return Promise.resolve(token); +} diff --git a/packages/common/src/newsletters-scheduler/index.ts b/packages/common/src/newsletters-scheduler/index.ts new file mode 100644 index 00000000000..d158c576401 --- /dev/null +++ b/packages/common/src/newsletters-scheduler/index.ts @@ -0,0 +1 @@ +export * from "./api"; diff --git a/packages/common/test/newsletters-scheduler/api/subscriptions.test.ts b/packages/common/test/newsletters-scheduler/api/subscriptions.test.ts new file mode 100644 index 00000000000..5ebcd58bf06 --- /dev/null +++ b/packages/common/test/newsletters-scheduler/api/subscriptions.test.ts @@ -0,0 +1,54 @@ +import { + ISubscription, + Cadence, + SystemNotificationSpecNames, + INotifyParams, + notify, + SubscriptionEntityType, + SubscriptionActions, +} from "../../../src/newsletters-scheduler"; +import * as authenticateRequestModule from "../../../src/newsletters-scheduler/api/utils/authenticate-request"; +import * as orvalModule from "../../../src/newsletters-scheduler/api/orval/api/orval-newsletters-scheduler"; + +describe("Subscriptions", () => { + const token = "aaa"; + let authenticateRequestSpy: any; + + beforeEach(() => { + authenticateRequestSpy = spyOn( + authenticateRequestModule, + "authenticateRequest" + ).and.callFake(async () => token); + }); + + describe("/subscriptions/notify", () => { + it("should notify", async () => { + const mockSubscription = { + subscription: "mock", + } as unknown as ISubscription[]; + const notifySpy = spyOn(orvalModule, "notify").and.callFake( + async () => mockSubscription + ); + + const options: INotifyParams = { + data: { + actions: [SubscriptionActions.DISCUSSION_POST_PENDING], + cadence: Cadence.WEEKLY, + notificationSpecName: + SystemNotificationSpecNames.DISCUSSION_ON_ENTITY, + entityId: "burrito", + entityType: SubscriptionEntityType.DISCUSSION, + }, + }; + + const result = await notify(options); + expect(result).toEqual(mockSubscription); + + expect(authenticateRequestSpy).toHaveBeenCalledWith(options); + expect(notifySpy).toHaveBeenCalledWith(options.data, { + ...options, + token, + }); + }); + }); +}); diff --git a/packages/common/test/newsletters-scheduler/api/utils/authenticate-request.test.ts b/packages/common/test/newsletters-scheduler/api/utils/authenticate-request.test.ts new file mode 100644 index 00000000000..ed55fa89cca --- /dev/null +++ b/packages/common/test/newsletters-scheduler/api/utils/authenticate-request.test.ts @@ -0,0 +1,36 @@ +import { IAuthenticationManager } from "@esri/arcgis-rest-request"; +import { INewslettersSchedulerRequestOptions } from "../../../../src/newsletters-scheduler"; +import { authenticateRequest } from "../../../../src/newsletters-scheduler/api/utils/authenticate-request"; + +describe("authenticateRequest", () => { + let getTokenSpy: any; + + const portal = "https://foo.com"; + const token = "aaa"; + const authentication = { + portal, + async getToken() { + return token; + }, + } as IAuthenticationManager; + + beforeEach(() => { + getTokenSpy = spyOn(authentication, "getToken").and.callThrough(); + }); + + it("returns params.token if provided", async () => { + const options: INewslettersSchedulerRequestOptions = { token: "bbb" }; + + const result = await authenticateRequest(options); + expect(result).toEqual("bbb"); + expect(getTokenSpy).not.toHaveBeenCalled(); + }); + + it("returns token from authentication if params.token not provided", async () => { + const options = { authentication } as INewslettersSchedulerRequestOptions; + + const result = await authenticateRequest(options); + expect(result).toEqual(token); + expect(getTokenSpy).toHaveBeenCalledWith(portal); + }); +});