@tstdl/base 0.93.114 → 0.93.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ import { Subject } from 'rxjs';
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3
+ import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
4
+ import { AUTHENTICATION_API_CLIENT } from '../../authentication/client/tokens.js';
5
+ import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
6
+ import { BadRequestError } from '../../errors/bad-request.error.js';
7
+ import { ForbiddenError } from '../../errors/forbidden.error.js';
8
+ import { InvalidTokenError } from '../../errors/invalid-token.error.js';
9
+ import { NotFoundError } from '../../errors/not-found.error.js';
10
+ import { NotSupportedError } from '../../errors/not-supported.error.js';
11
+ import { UnauthorizedError } from '../../errors/unauthorized.error.js';
12
+ import { Injector } from '../../injector/index.js';
13
+ import { Lock } from '../../lock/index.js';
14
+ import { Logger } from '../../logger/index.js';
15
+ import { MessageBus } from '../../message-bus/index.js';
16
+ import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
17
+ import { timeout } from '../../utils/timing.js';
18
+ describe('AuthenticationClientService Error Handling & Stuck States', () => {
19
+ let injector;
20
+ let service;
21
+ let mockApiClient;
22
+ let mockLock;
23
+ let mockMessageBus;
24
+ let mockLogger;
25
+ let disposeToken;
26
+ beforeEach(() => {
27
+ const storage = new Map();
28
+ globalThis.localStorage = {
29
+ getItem: vi.fn((key) => storage.get(key) ?? null),
30
+ setItem: vi.fn((key, value) => storage.set(key, value)),
31
+ removeItem: vi.fn((key) => storage.delete(key)),
32
+ clear: vi.fn(() => storage.clear()),
33
+ };
34
+ configureDefaultSignalsImplementation();
35
+ injector = new Injector('Test');
36
+ mockApiClient = {
37
+ login: vi.fn(),
38
+ refresh: vi.fn(),
39
+ timestamp: vi.fn().mockResolvedValue(Math.floor(Date.now() / 1000)),
40
+ endSession: vi.fn().mockResolvedValue(undefined),
41
+ };
42
+ mockLock = {
43
+ tryUse: vi.fn(async (_timeout, callback) => {
44
+ const result = await callback({ lost: false });
45
+ return { success: true, result };
46
+ }),
47
+ use: vi.fn(async (_timeout, callback) => {
48
+ return await callback({ lost: false });
49
+ }),
50
+ };
51
+ mockMessageBus = {
52
+ publishAndForget: vi.fn(),
53
+ messages$: new Subject(),
54
+ dispose: vi.fn(),
55
+ allMessages$: new Subject(),
56
+ };
57
+ mockLogger = {
58
+ error: vi.fn(),
59
+ warn: vi.fn(),
60
+ info: vi.fn(),
61
+ debug: vi.fn(),
62
+ };
63
+ injector.register(AUTHENTICATION_API_CLIENT, { useValue: mockApiClient });
64
+ injector.register(Lock, { useValue: mockLock });
65
+ injector.register(MessageBus, { useValue: mockMessageBus });
66
+ injector.register(Logger, { useValue: mockLogger });
67
+ disposeToken = new CancellationToken();
68
+ injector.register(CancellationSignal, { useValue: disposeToken.signal });
69
+ });
70
+ afterEach(async () => {
71
+ await service.dispose();
72
+ });
73
+ test('Corrupt Storage: should handle invalid JSON in storage gracefully', async () => {
74
+ // initialize with corrupt token
75
+ globalThis.localStorage.setItem('AuthenticationService:token', '{ invalid-json');
76
+ service = injector.resolve(AuthenticationClientService);
77
+ expect(service.token()).toBeUndefined();
78
+ expect(mockLogger.warn).toHaveBeenCalled(); // Should warn about parse error
79
+ });
80
+ test('Unrecoverable Errors: should logout on InvalidTokenError during refresh', async () => {
81
+ const now = Math.floor(Date.now() / 1000);
82
+ const initialToken = { exp: now + 5, jti: 'initial' };
83
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
84
+ service = injector.resolve(AuthenticationClientService);
85
+ mockApiClient.refresh.mockRejectedValue(new InvalidTokenError());
86
+ // Wait for loop to pick up refresh
87
+ await timeout(100);
88
+ expect(mockApiClient.endSession).toHaveBeenCalled();
89
+ expect(service.token()).toBeUndefined();
90
+ });
91
+ test('Unrecoverable Errors: should logout on NotFoundError during refresh', async () => {
92
+ const now = Math.floor(Date.now() / 1000);
93
+ const initialToken = { exp: now + 5, jti: 'initial' };
94
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
95
+ service = injector.resolve(AuthenticationClientService);
96
+ mockApiClient.refresh.mockRejectedValue(new NotFoundError('Session not found'));
97
+ await timeout(100);
98
+ expect(mockApiClient.endSession).toHaveBeenCalled();
99
+ expect(service.token()).toBeUndefined();
100
+ });
101
+ test('Unrecoverable Errors: should logout on ForbiddenError during refresh', async () => {
102
+ const now = Math.floor(Date.now() / 1000);
103
+ const initialToken = { exp: now + 5, jti: 'initial' };
104
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
105
+ service = injector.resolve(AuthenticationClientService);
106
+ mockApiClient.refresh.mockRejectedValue(new ForbiddenError());
107
+ await timeout(100);
108
+ expect(mockApiClient.endSession).toHaveBeenCalled();
109
+ expect(service.token()).toBeUndefined();
110
+ });
111
+ test('Unrecoverable Errors: should logout on UnauthorizedError during refresh', async () => {
112
+ const now = Math.floor(Date.now() / 1000);
113
+ const initialToken = { exp: now + 5, jti: 'initial' };
114
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
115
+ service = injector.resolve(AuthenticationClientService);
116
+ mockApiClient.refresh.mockRejectedValue(new UnauthorizedError());
117
+ await timeout(100);
118
+ expect(mockApiClient.endSession).toHaveBeenCalled();
119
+ expect(service.token()).toBeUndefined();
120
+ });
121
+ test('Unrecoverable Errors: should logout on NotSupportedError during refresh', async () => {
122
+ const now = Math.floor(Date.now() / 1000);
123
+ const initialToken = { exp: now + 5, jti: 'initial' };
124
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
125
+ service = injector.resolve(AuthenticationClientService);
126
+ mockApiClient.refresh.mockRejectedValue(new NotSupportedError());
127
+ await timeout(100);
128
+ expect(mockApiClient.endSession).toHaveBeenCalled();
129
+ expect(service.token()).toBeUndefined();
130
+ });
131
+ test('Unrecoverable Errors: should logout on BadRequestError during refresh', async () => {
132
+ const now = Math.floor(Date.now() / 1000);
133
+ const initialToken = { exp: now + 5, jti: 'initial' };
134
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
135
+ service = injector.resolve(AuthenticationClientService);
136
+ mockApiClient.refresh.mockRejectedValue(new BadRequestError());
137
+ await timeout(100);
138
+ expect(mockApiClient.endSession).toHaveBeenCalled();
139
+ expect(service.token()).toBeUndefined();
140
+ });
141
+ test('Recoverable Errors: should NOT logout on generic Error during refresh', async () => {
142
+ const now = Math.floor(Date.now() / 1000);
143
+ const initialToken = { exp: now + 5, jti: 'initial' };
144
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
145
+ service = injector.resolve(AuthenticationClientService);
146
+ mockApiClient.refresh.mockRejectedValue(new Error('Network failure'));
147
+ await timeout(100);
148
+ // Should NOT have called endSession (logout)
149
+ expect(mockApiClient.endSession).not.toHaveBeenCalled();
150
+ // Token should still be present (retry logic handles it)
151
+ expect(service.token()).toBeDefined();
152
+ });
153
+ test('Logout Failure: should clear local token even if server logout fails', async () => {
154
+ const now = Math.floor(Date.now() / 1000);
155
+ const initialToken = { exp: now + 1000, jti: 'initial' };
156
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
157
+ service = injector.resolve(AuthenticationClientService);
158
+ mockApiClient.endSession.mockRejectedValue(new Error('Network error during logout'));
159
+ await service.logout();
160
+ expect(mockApiClient.endSession).toHaveBeenCalled();
161
+ expect(service.token()).toBeUndefined();
162
+ expect(mockMessageBus.publishAndForget).toHaveBeenCalled(); // loggedOutBus
163
+ });
164
+ });
@@ -1,7 +1,7 @@
1
1
  import { of } from 'rxjs';
2
2
  import { describe, expect, test, vi } from 'vitest';
3
- import { HttpClientRequest } from '../../http/client/index.js';
4
- import { waitForAuthenticationCredentialsMiddleware } from '../client/http-client.middleware.js';
3
+ import { HttpClientRequest, HttpClientResponse, HttpError, HttpErrorReason } from '../../http/index.js';
4
+ import { logoutOnUnauthorizedMiddleware, waitForAuthenticationCredentialsMiddleware } from '../client/http-client.middleware.js';
5
5
  describe('waitForAuthenticationCredentialsMiddleware', () => {
6
6
  test('should wait for token and call next', async () => {
7
7
  const authenticationServiceMock = {
@@ -21,3 +21,35 @@ describe('waitForAuthenticationCredentialsMiddleware', () => {
21
21
  expect(next).toHaveBeenCalled();
22
22
  });
23
23
  });
24
+ describe('logoutOnUnauthorizedMiddleware', () => {
25
+ test('should call logout on 401 error', async () => {
26
+ const authenticationServiceMock = {
27
+ logout: vi.fn().mockResolvedValue(undefined),
28
+ };
29
+ const middleware = logoutOnUnauthorizedMiddleware(authenticationServiceMock);
30
+ const request = new HttpClientRequest('http://localhost');
31
+ const response = new HttpClientResponse({
32
+ request,
33
+ statusCode: 401,
34
+ statusMessage: 'Unauthorized',
35
+ headers: {},
36
+ body: undefined,
37
+ closeHandler: () => { }
38
+ });
39
+ const error = new HttpError(HttpErrorReason.StatusCode, request, { response });
40
+ const next = vi.fn().mockRejectedValue(error);
41
+ await expect(middleware({ request }, next)).rejects.toThrow(HttpError);
42
+ expect(authenticationServiceMock.logout).toHaveBeenCalled();
43
+ });
44
+ test('should not call logout on other errors', async () => {
45
+ const authenticationServiceMock = {
46
+ logout: vi.fn().mockResolvedValue(undefined),
47
+ };
48
+ const middleware = logoutOnUnauthorizedMiddleware(authenticationServiceMock);
49
+ const request = new HttpClientRequest('http://localhost');
50
+ const error = new Error('Some other error');
51
+ const next = vi.fn().mockRejectedValue(error);
52
+ await expect(middleware({ request }, next)).rejects.toThrow('Some other error');
53
+ expect(authenticationServiceMock.logout).not.toHaveBeenCalled();
54
+ });
55
+ });
@@ -19,6 +19,7 @@ export declare const notificationApiDefinition: {
19
19
  parameters: import("../../schema/index.js").ObjectSchema<{
20
20
  offset?: number | undefined;
21
21
  limit?: number | undefined;
22
+ unreadOnly?: boolean | undefined;
22
23
  includeArchived?: boolean | undefined;
23
24
  }>;
24
25
  result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
@@ -33,6 +34,12 @@ export declare const notificationApiDefinition: {
33
34
  result: import("../../schema/index.js").LiteralSchema<"ok">;
34
35
  credentials: true;
35
36
  };
37
+ markAllRead: {
38
+ resource: string;
39
+ method: "POST";
40
+ result: import("../../schema/index.js").LiteralSchema<"ok">;
41
+ credentials: true;
42
+ };
36
43
  archive: {
37
44
  resource: string;
38
45
  method: "POST";
@@ -42,6 +49,18 @@ export declare const notificationApiDefinition: {
42
49
  result: import("../../schema/index.js").LiteralSchema<"ok">;
43
50
  credentials: true;
44
51
  };
52
+ archiveAll: {
53
+ resource: string;
54
+ method: "POST";
55
+ result: import("../../schema/index.js").LiteralSchema<"ok">;
56
+ credentials: true;
57
+ };
58
+ unreadCount: {
59
+ resource: string;
60
+ method: "GET";
61
+ result: import("../../schema/index.js").NumberSchema;
62
+ credentials: true;
63
+ };
45
64
  getPreferences: {
46
65
  resource: string;
47
66
  method: "GET";
@@ -96,6 +115,7 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
96
115
  parameters: import("../../schema/index.js").ObjectSchema<{
97
116
  offset?: number | undefined;
98
117
  limit?: number | undefined;
118
+ unreadOnly?: boolean | undefined;
99
119
  includeArchived?: boolean | undefined;
100
120
  }>;
101
121
  result: import("../../schema/index.js").ArraySchema<InAppNotificationView<Record<string, import("../models/notification-log.model.js").NotificationDefinition<import("../../types/types.js").ObjectLiteral, import("../../types/types.js").ObjectLiteral>>>>;
@@ -110,6 +130,12 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
110
130
  result: import("../../schema/index.js").LiteralSchema<"ok">;
111
131
  credentials: true;
112
132
  };
133
+ markAllRead: {
134
+ resource: string;
135
+ method: "POST";
136
+ result: import("../../schema/index.js").LiteralSchema<"ok">;
137
+ credentials: true;
138
+ };
113
139
  archive: {
114
140
  resource: string;
115
141
  method: "POST";
@@ -119,6 +145,18 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
119
145
  result: import("../../schema/index.js").LiteralSchema<"ok">;
120
146
  credentials: true;
121
147
  };
148
+ archiveAll: {
149
+ resource: string;
150
+ method: "POST";
151
+ result: import("../../schema/index.js").LiteralSchema<"ok">;
152
+ credentials: true;
153
+ };
154
+ unreadCount: {
155
+ resource: string;
156
+ method: "GET";
157
+ result: import("../../schema/index.js").NumberSchema;
158
+ credentials: true;
159
+ };
122
160
  getPreferences: {
123
161
  resource: string;
124
162
  method: "GET";
@@ -26,6 +26,7 @@ export const notificationApiDefinition = defineApi({
26
26
  parameters: object({
27
27
  limit: optional(number()),
28
28
  offset: optional(number()),
29
+ unreadOnly: optional(boolean()),
29
30
  includeArchived: optional(boolean()),
30
31
  }),
31
32
  result: array(InAppNotificationView),
@@ -38,6 +39,12 @@ export const notificationApiDefinition = defineApi({
38
39
  result: literal('ok'),
39
40
  credentials: true,
40
41
  },
42
+ markAllRead: {
43
+ resource: 'in-app/read-all',
44
+ method: 'POST',
45
+ result: literal('ok'),
46
+ credentials: true,
47
+ },
41
48
  archive: {
42
49
  resource: 'in-app/:id/archive',
43
50
  method: 'POST',
@@ -45,6 +52,18 @@ export const notificationApiDefinition = defineApi({
45
52
  result: literal('ok'),
46
53
  credentials: true,
47
54
  },
55
+ archiveAll: {
56
+ resource: 'in-app/archive-all',
57
+ method: 'POST',
58
+ result: literal('ok'),
59
+ credentials: true,
60
+ },
61
+ unreadCount: {
62
+ resource: 'in-app/unread-count',
63
+ method: 'GET',
64
+ result: number(),
65
+ credentials: true,
66
+ },
48
67
  getPreferences: {
49
68
  resource: 'preferences',
50
69
  method: 'GET',
@@ -8,7 +8,10 @@ export declare class NotificationApiController implements ApiController<Notifica
8
8
  stream({ abortSignal, getToken }: ApiRequestContext<NotificationApiDefinition, 'stream'>): ApiServerResult<NotificationApiDefinition, 'stream'>;
9
9
  listInApp({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'listInApp'>): Promise<any>;
10
10
  markRead({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'markRead'>): Promise<'ok'>;
11
+ markAllRead({ getToken }: ApiRequestContext<NotificationApiDefinition, 'markAllRead'>): Promise<'ok'>;
11
12
  archive({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'archive'>): Promise<'ok'>;
13
+ archiveAll({ getToken }: ApiRequestContext<NotificationApiDefinition, 'archiveAll'>): Promise<'ok'>;
14
+ unreadCount({ getToken }: ApiRequestContext<NotificationApiDefinition, 'unreadCount'>): Promise<number>;
12
15
  getPreferences({ getToken }: ApiRequestContext<NotificationApiDefinition, 'getPreferences'>): Promise<any>;
13
16
  updatePreference({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'updatePreference'>): Promise<'ok'>;
14
17
  registerWebPush({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'registerWebPush'>): Promise<'ok'>;
@@ -35,11 +35,25 @@ let NotificationApiController = class NotificationApiController {
35
35
  await this.notificationService.markRead(token.payload.tenant, token.payload.subject, parameters.id);
36
36
  return 'ok';
37
37
  }
38
+ async markAllRead({ getToken }) {
39
+ const token = await getToken();
40
+ await this.notificationService.markAllRead(token.payload.tenant, token.payload.subject);
41
+ return 'ok';
42
+ }
38
43
  async archive({ parameters, getToken }) {
39
44
  const token = await getToken();
40
45
  await this.notificationService.archive(token.payload.tenant, token.payload.subject, parameters.id);
41
46
  return 'ok';
42
47
  }
48
+ async archiveAll({ getToken }) {
49
+ const token = await getToken();
50
+ await this.notificationService.archiveAll(token.payload.tenant, token.payload.subject);
51
+ return 'ok';
52
+ }
53
+ async unreadCount({ getToken }) {
54
+ const token = await getToken();
55
+ return await this.notificationService.unreadCount(token.payload.tenant, token.payload.subject);
56
+ }
43
57
  async getPreferences({ getToken }) {
44
58
  const token = await getToken();
45
59
  return await this.notificationService.getPreferences(token.payload.tenant, token.payload.subject);
@@ -5,7 +5,7 @@ import { NotificationAncillaryService } from './services/notification-ancillary.
5
5
  export declare class NotificationConfiguration {
6
6
  database?: DatabaseConfig;
7
7
  defaultChannels?: NotificationChannel[];
8
- ancillaryService?: InjectionToken<NotificationAncillaryService>;
8
+ ancillaryService?: InjectionToken<NotificationAncillaryService<any>>;
9
9
  }
10
10
  export declare function configureNotification({ injector, ...config }: NotificationConfiguration & {
11
11
  injector?: Injector;
@@ -1,5 +1,5 @@
1
1
  import { Transactional } from '../../../orm/server/index.js';
2
2
  import type { NotificationDefinitionMap, NotificationLog, NotificationView } from '../../models/index.js';
3
3
  export declare abstract class NotificationAncillaryService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
4
- abstract getViewData(notifications: NotificationLog<Definitions>[]): Promise<NotificationView<Definitions>[]>;
4
+ abstract getViewData(tenantId: string, notifications: NotificationLog<Definitions>[]): Promise<NotificationView<Definitions>[]>;
5
5
  }
@@ -79,7 +79,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
79
79
  }
80
80
  for (const channel of enabledChannels) {
81
81
  // TODO: what if an error occurs here? partial delivery? Should we use tasks to allow retrying individual channels?
82
- const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
82
+ const [viewData] = await this.#notificationAncillaryService.getViewData(notification.tenantId, [notification]);
83
83
  await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, channel, tx);
84
84
  }
85
85
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { status: NotificationStatus.Sent, currentStep: 1 });
@@ -98,7 +98,7 @@ let NotificationDeliveryWorker = class NotificationDeliveryWorker extends Transa
98
98
  this.#logger.debug(`Notification ${notificationId} already read, skipping escalation step ${step}`);
99
99
  return TaskProcessResult.Complete();
100
100
  }
101
- const [viewData] = await this.#notificationAncillaryService.getViewData([notification]);
101
+ const [viewData] = await this.#notificationAncillaryService.getViewData(notification.tenantId, [notification]);
102
102
  await this.sendToChannel({ ...notification, payload: { ...notification.payload, ...viewData } }, rule.channel, tx);
103
103
  await this.#notificationLogRepository.withTransaction(tx).update(notificationId, { currentStep: step + 1 });
104
104
  if (step < type.escalations.length) {
@@ -1,17 +1,24 @@
1
1
  import { type NewEntity } from '../../../orm/index.js';
2
- import { Transactional } from '../../../orm/server/index.js';
2
+ import { Transactional, type Transaction } from '../../../orm/server/index.js';
3
3
  import type { TypedOmit } from '../../../types/types.js';
4
4
  import { NotificationPreference, type InAppNotificationView, type NotificationChannel, type NotificationDefinitionMap, type NotificationLog } from '../../models/index.js';
5
+ export type NewNotificationData<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> = TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'timestamp' | 'status' | 'currentStep' | 'priority'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>;
5
6
  export declare class NotificationService<Definitions extends NotificationDefinitionMap = NotificationDefinitionMap> extends Transactional {
6
7
  #private;
7
- send(tenantId: string, userId: string, notification: TypedOmit<NewEntity<NotificationLog<Definitions>>, 'tenantId' | 'id' | 'userId' | 'timestamp' | 'status' | 'currentStep' | 'priority'> & Partial<Pick<NotificationLog<Definitions>, 'priority'>>): Promise<void>;
8
+ send(tenantId: string, userId: string, notification: NewNotificationData<Definitions>, options?: {
9
+ transaction: Transaction;
10
+ }): Promise<void>;
8
11
  listInApp(tenantId: string, userId: string, options?: {
9
12
  limit?: number;
10
13
  offset?: number;
11
14
  includeArchived?: boolean;
15
+ unreadOnly?: boolean;
12
16
  }): Promise<InAppNotificationView[]>;
13
17
  markRead(tenantId: string, userId: string, id: string): Promise<void>;
18
+ markAllRead(tenantId: string, userId: string): Promise<void>;
14
19
  archive(tenantId: string, userId: string, id: string): Promise<void>;
20
+ archiveAll(tenantId: string, userId: string): Promise<void>;
21
+ unreadCount(tenantId: string, userId: string): Promise<number>;
15
22
  getPreferences(tenantId: string, userId: string): Promise<NotificationPreference[]>;
16
23
  updatePreference(tenantId: string, userId: string, type: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
17
24
  registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: Uint8Array<ArrayBuffer>, auth: Uint8Array<ArrayBuffer>): Promise<void>;
@@ -20,8 +20,8 @@ let NotificationService = class NotificationService extends Transactional {
20
20
  #webPushSubscriptionRepository = injectRepository(WebPushSubscription);
21
21
  #notificationAncillaryService = inject(NotificationAncillaryService);
22
22
  #taskQueue = inject((TaskQueue), 'notification');
23
- async send(tenantId, userId, notification) {
24
- await this.transaction(async (tx) => {
23
+ async send(tenantId, userId, notification, options) {
24
+ await this.useTransaction(options?.transaction, async (tx) => {
25
25
  const notificationToInsert = {
26
26
  tenantId,
27
27
  userId,
@@ -43,7 +43,7 @@ let NotificationService = class NotificationService extends Transactional {
43
43
  })
44
44
  .from(notificationLog)
45
45
  .innerJoin(inAppNotification, and(eq(inAppNotification.tenantId, notificationLog.tenantId), eq(inAppNotification.userId, notificationLog.userId), eq(inAppNotification.logId, notificationLog.id)))
46
- .where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp)))
46
+ .where(and(eq(notificationLog.tenantId, tenantId), eq(notificationLog.userId, userId), options.includeArchived ? undefined : isNull(inAppNotification.archiveTimestamp), options.unreadOnly ? isNull(inAppNotification.readTimestamp) : undefined))
47
47
  .limit(options.limit ?? 50)
48
48
  .offset(options.offset ?? 0)
49
49
  .orderBy(desc(notificationLog.timestamp));
@@ -51,7 +51,7 @@ let NotificationService = class NotificationService extends Transactional {
51
51
  const notificationRows = rows.map((row) => row.notification);
52
52
  const notificationEntities = await this.#notificationLogRepository.mapManyToEntity(notificationRows);
53
53
  const inAppEntities = await this.#inAppNotificationRepository.mapManyToEntity(inAppRows);
54
- const notificationViewDatas = await this.#notificationAncillaryService.getViewData(notificationEntities);
54
+ const notificationViewDatas = await this.#notificationAncillaryService.getViewData(tenantId, notificationEntities);
55
55
  const views = notificationEntities.map((notification, index) => {
56
56
  const inApp = inAppEntities[index];
57
57
  const viewData = notificationViewDatas[index];
@@ -63,9 +63,23 @@ let NotificationService = class NotificationService extends Transactional {
63
63
  async markRead(tenantId, userId, id) {
64
64
  await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { readTimestamp: TRANSACTION_TIMESTAMP });
65
65
  }
66
+ async markAllRead(tenantId, userId) {
67
+ await this.#inAppNotificationRepository.updateByQuery({ tenantId, userId, readTimestamp: null }, { readTimestamp: TRANSACTION_TIMESTAMP });
68
+ }
66
69
  async archive(tenantId, userId, id) {
67
70
  await this.#inAppNotificationRepository.updateByQuery({ tenantId, id, userId }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
68
71
  }
72
+ async archiveAll(tenantId, userId) {
73
+ await this.#inAppNotificationRepository.updateByQuery({ tenantId, userId, archiveTimestamp: null }, { archiveTimestamp: TRANSACTION_TIMESTAMP });
74
+ }
75
+ async unreadCount(tenantId, userId) {
76
+ return await this.#inAppNotificationRepository.countByQuery({
77
+ tenantId,
78
+ userId,
79
+ readTimestamp: null,
80
+ archiveTimestamp: null,
81
+ });
82
+ }
69
83
  async getPreferences(tenantId, userId) {
70
84
  return await this.#preferenceRepository.loadManyByQuery({ tenantId, userId });
71
85
  }
@@ -18,7 +18,7 @@ import { NotificationDeliveryWorker } from '../server/services/notification-deli
18
18
  import { NotificationTypeService } from '../server/services/notification-type.service.js';
19
19
  import { NotificationService } from '../server/services/notification.service.js';
20
20
  let MockNotificationAncillaryService = class MockNotificationAncillaryService extends NotificationAncillaryService {
21
- async getViewData(notifications) {
21
+ async getViewData(_tenantId, notifications) {
22
22
  return notifications.map(() => ({}));
23
23
  }
24
24
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.114",
3
+ "version": "0.93.116",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -172,7 +172,7 @@
172
172
  "preact": "^10.28",
173
173
  "preact-render-to-string": "^6.6",
174
174
  "sharp": "^0.34",
175
- "undici": "^7.20",
175
+ "undici": "^7.21",
176
176
  "urlpattern-polyfill": "^10.1",
177
177
  "zod": "^3.25"
178
178
  },
@@ -182,7 +182,7 @@
182
182
  }
183
183
  },
184
184
  "devDependencies": {
185
- "@stylistic/eslint-plugin": "5.7",
185
+ "@stylistic/eslint-plugin": "5.8",
186
186
  "@types/koa__router": "12.0",
187
187
  "@types/luxon": "3.7",
188
188
  "@types/mjml": "4.7",
@@ -197,11 +197,11 @@
197
197
  "globals": "17.3",
198
198
  "tsc-alias": "1.8",
199
199
  "typedoc-github-wiki-theme": "2.1",
200
- "typedoc-plugin-markdown": "4.9",
200
+ "typedoc-plugin-markdown": "4.10",
201
201
  "typedoc-plugin-missing-exports": "4.1",
202
202
  "typescript": "5.9",
203
203
  "typescript-eslint": "8.54",
204
- "vite-tsconfig-paths": "6.0",
204
+ "vite-tsconfig-paths": "6.1",
205
205
  "vitest": "4.0"
206
206
  },
207
207
  "overrides": {