Skip to content

Commit

Permalink
Revert "Revert "fix(js): Remove @novu/shared dependency" (#7206)"
Browse files Browse the repository at this point in the history
This reverts commit adde8dd.
  • Loading branch information
SokratisVidros committed Dec 4, 2024
1 parent 184c549 commit d1c3865
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 112 deletions.
5 changes: 5 additions & 0 deletions packages/js/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
module.exports = {
preset: 'ts-jest',
setupFiles: ['./jest.setup.ts'],
globals: {
NOVU_API_VERSION: '2024-06-26',
PACKAGE_NAME: '@novu/js',
PACKAGE_VERSION: 'test',
},
};
2 changes: 0 additions & 2 deletions packages/js/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
global.PACKAGE_VERSION = 'test-version';
global.PACKAGE_NAME = 'test-package';
1 change: 0 additions & 1 deletion packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@
},
"dependencies": {
"@floating-ui/dom": "^1.6.7",
"@novu/client": "workspace:*",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"mitt": "^3.0.1",
Expand Down
119 changes: 119 additions & 0 deletions packages/js/src/api/http-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
export type HttpClientOptions = {
apiVersion?: string;
backendUrl?: string;
userAgent?: string;
};

const DEFAULT_API_VERSION = 'v1';
const DEFAULT_BACKEND_URL = 'https://api.novu.co';
const DEFAULT_USER_AGENT = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;

export class HttpClient {
private backendUrl: string;
private apiVersion: string;
private headers: Record<string, string>;

constructor(options: HttpClientOptions = {}) {
const {
apiVersion = DEFAULT_API_VERSION,
backendUrl = DEFAULT_BACKEND_URL,
userAgent = DEFAULT_USER_AGENT,
} = options || {};
this.apiVersion = apiVersion;
this.backendUrl = `${backendUrl}/${this.apiVersion}`;
this.headers = {
'Novu-API-Version': NOVU_API_VERSION,
'Content-Type': 'application/json',
'User-Agent': userAgent,
};
}

setAuthorizationToken(token: string) {
this.headers.Authorization = `Bearer ${token}`;
}

setHeaders(headers: Record<string, string>) {
this.headers = {
...this.headers,
...headers,
};
}

async get<T>(path: string, searchParams?: URLSearchParams, unwrapEnvelope = true) {
return this.doFetch<T>({
path,
searchParams,
options: {
method: 'GET',
},
unwrapEnvelope,
});
}

async post<T>(path: string, body?: any) {
return this.doFetch<T>({
path,
options: {
method: 'POST',
body,
},
});
}

async patch<T>(path: string, body?: any) {
return this.doFetch<T>({
path,
options: {
method: 'PATCH',
body,
},
});
}

async delete<T>(path: string, body?: any) {
return this.doFetch<T>({
path,
options: {
method: 'DELETE',
body,
},
});
}

private async doFetch<T>({
path,
searchParams,
options,
unwrapEnvelope = true,
}: {
path: string;
searchParams?: URLSearchParams;
options?: RequestInit;
unwrapEnvelope?: boolean;
}) {
const fullUrl = combineUrl(this.backendUrl, path, searchParams ? `?${searchParams.toString()}` : '');
const reqInit = {
method: options?.method || 'GET',
headers: { ...this.headers, ...(options?.headers || {}) },
body: options?.body ? JSON.stringify(options.body) : undefined,
};

const response = await fetch(fullUrl, reqInit);

if (!response.ok) {
const errorData = await response.json();
throw new Error(`${this.headers['User-Agent']} error. Status: ${response.status}, Message: ${errorData.message}`);
}
if (response.status === 204) {
return undefined as unknown as T;
}

const res = await response.json();

return (unwrapEnvelope ? res.data : res) as Promise<T>;
}
}

function combineUrl(...args: string[]): string {
return args.map((part) => part.replace(/^\/+|\/+$/g, '')).join('/');
}
31 changes: 16 additions & 15 deletions packages/js/src/api/inbox-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ApiOptions, HttpClient } from '@novu/client';
import type {
ActionTypeEnum,
ChannelPreference,
Expand All @@ -7,10 +6,10 @@ import type {
PreferencesResponse,
Session,
} from '../types';
import { HttpClient, HttpClientOptions } from './http-client';

export type InboxServiceOptions = ApiOptions;
export type InboxServiceOptions = HttpClientOptions;

const NOVU_API_VERSION = '2024-06-26';
const INBOX_ROUTE = '/inbox';
const INBOX_NOTIFICATIONS_ROUTE = `${INBOX_ROUTE}/notifications`;

Expand All @@ -20,10 +19,6 @@ export class InboxService {

constructor(options: InboxServiceOptions = {}) {
this.#httpClient = new HttpClient(options);
this.#httpClient.updateHeaders({
'Novu-API-Version': NOVU_API_VERSION,
'Novu-User-Agent': options.userAgent || '@novu/js',
});
}

async initializeSession({
Expand Down Expand Up @@ -61,24 +56,24 @@ export class InboxService {
after?: string;
offset?: number;
}): Promise<{ data: InboxNotification[]; hasMore: boolean; filter: NotificationFilter }> {
const queryParams = new URLSearchParams(`limit=${limit}`);
const searchParams = new URLSearchParams(`limit=${limit}`);
if (after) {
queryParams.append('after', after);
searchParams.append('after', after);
}
if (offset) {
queryParams.append('offset', `${offset}`);
searchParams.append('offset', `${offset}`);
}
if (tags) {
tags.forEach((tag) => queryParams.append('tags[]', tag));
tags.forEach((tag) => searchParams.append('tags[]', tag));
}
if (read !== undefined) {
queryParams.append('read', `${read}`);
searchParams.append('read', `${read}`);
}
if (archived !== undefined) {
queryParams.append('archived', `${archived}`);
searchParams.append('archived', `${archived}`);
}

return this.#httpClient.getFullResponse(`${INBOX_NOTIFICATIONS_ROUTE}?${queryParams.toString()}`);
return this.#httpClient.get(INBOX_NOTIFICATIONS_ROUTE, searchParams, false);
}

count({ filters }: { filters: Array<{ tags?: string[]; read?: boolean; archived?: boolean }> }): Promise<{
Expand All @@ -87,7 +82,13 @@ export class InboxService {
filter: NotificationFilter;
}>;
}> {
return this.#httpClient.getFullResponse(`${INBOX_NOTIFICATIONS_ROUTE}/count?filters=${JSON.stringify(filters)}`);
return this.#httpClient.get(
`${INBOX_NOTIFICATIONS_ROUTE}/count`,
new URLSearchParams({
filters: JSON.stringify(filters),
}),
false
);
}

read(notificationId: string): Promise<InboxNotification> {
Expand Down
71 changes: 71 additions & 0 deletions packages/js/src/base-module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { InboxService } from './api';
import { BaseModule } from './base-module';
import { NovuEventEmitter } from './event-emitter';

beforeAll(() => jest.spyOn(global, 'fetch'));
afterAll(() => jest.restoreAllMocks());

describe('callWithSession(fn)', () => {
test('should invoke callback function immediately if session is initialized', async () => {
const emitter = new NovuEventEmitter();
const bm = new BaseModule({
inboxServiceInstance: {
isSessionInitialized: true,
} as InboxService,
eventEmitterInstance: emitter,
});

const cb = jest.fn();
bm.callWithSession(cb);
expect(cb).toHaveBeenCalled();
});

test('should invoke callback function as soon as session is initialized', async () => {
const emitter = new NovuEventEmitter();
const bm = new BaseModule({
inboxServiceInstance: {} as InboxService,
eventEmitterInstance: emitter,
});

const cb = jest.fn();

bm.callWithSession(cb);
expect(cb).not.toHaveBeenCalled();

emitter.emit('session.initialize.resolved', {
args: {
applicationIdentifier: 'foo',
subscriberId: 'bar',
},
data: {
token: 'cafebabe',
totalUnreadCount: 10,
removeNovuBranding: true,
},
});

expect(cb).toHaveBeenCalled();
});

test('should return an error if session initialization failed', async () => {
const emitter = new NovuEventEmitter();
const bm = new BaseModule({
inboxServiceInstance: {} as InboxService,
eventEmitterInstance: emitter,
});

emitter.emit('session.initialize.resolved', {
args: {
applicationIdentifier: 'foo',
subscriberId: 'bar',
},
error: new Error('Failed to initialize session'),
});

const cb = jest.fn();
const result = await bm.callWithSession(cb);
expect(result).toEqual({
error: new Error('Failed to initialize session, please contact the support'),
});
});
});
11 changes: 6 additions & 5 deletions packages/js/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable vars-on-top */
/* eslint-disable no-var */
import { Novu } from './novu';
import type { Novu } from './novu';

export {};

declare global {
var PACKAGE_NAME: string;
var PACKAGE_VERSION: string;
const NOVU_API_VERSION: string;
const PACKAGE_NAME: string;
const PACKAGE_VERSION: string;
interface Window {
Novu: typeof Novu;
}
Expand Down
Loading

0 comments on commit d1c3865

Please sign in to comment.