Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(js): Remove @novu/shared dependency #6906

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@novu/api",
"version": "2.1.0",
"version": "2.1.1",
"description": "description",
"author": "",
"private": "true",
Expand Down
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
Loading