vintasend 0.5.2 → 0.6.0

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.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@ export type { LocalFileAttachmentManagerConfig } from './services/attachment-man
3
3
  export { LocalFileAttachmentManager } from './services/attachment-manager/local-file-attachment-manager';
4
4
  export { BaseNotificationAdapter, isOneOffNotification, } from './services/notification-adapters/base-notification-adapter';
5
5
  export type { BaseNotificationBackend } from './services/notification-backends/base-notification-backend';
6
- export { supportsAttachments } from './services/notification-backends/base-notification-backend';
6
+ export { supportsAttachments, isFieldFilter } from './services/notification-backends/base-notification-backend';
7
+ export type { NotificationFilter, NotificationFilterFields, DateRange, NotificationFilterCapabilities } from './services/notification-backends/base-notification-backend';
7
8
  export type { BaseNotificationQueueService } from './services/notification-queue-service/base-notification-queue-service';
8
9
  export type { VintaSend } from './services/notification-service';
9
10
  export { VintaSendFactory } from './services/notification-service';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isAttachmentReference = exports.VintaSendFactory = exports.supportsAttachments = exports.isOneOffNotification = exports.BaseNotificationAdapter = exports.LocalFileAttachmentManager = exports.BaseAttachmentManager = void 0;
3
+ exports.isAttachmentReference = exports.VintaSendFactory = exports.isFieldFilter = exports.supportsAttachments = exports.isOneOffNotification = exports.BaseNotificationAdapter = exports.LocalFileAttachmentManager = exports.BaseAttachmentManager = void 0;
4
4
  // Attachment Manager
5
5
  var base_attachment_manager_1 = require("./services/attachment-manager/base-attachment-manager");
6
6
  Object.defineProperty(exports, "BaseAttachmentManager", { enumerable: true, get: function () { return base_attachment_manager_1.BaseAttachmentManager; } });
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "BaseNotificationAdapter", { enumerable: true, ge
11
11
  Object.defineProperty(exports, "isOneOffNotification", { enumerable: true, get: function () { return base_notification_adapter_1.isOneOffNotification; } });
12
12
  var base_notification_backend_1 = require("./services/notification-backends/base-notification-backend");
13
13
  Object.defineProperty(exports, "supportsAttachments", { enumerable: true, get: function () { return base_notification_backend_1.supportsAttachments; } });
14
+ Object.defineProperty(exports, "isFieldFilter", { enumerable: true, get: function () { return base_notification_backend_1.isFieldFilter; } });
14
15
  var notification_service_1 = require("./services/notification-service");
15
16
  Object.defineProperty(exports, "VintaSendFactory", { enumerable: true, get: function () { return notification_service_1.VintaSendFactory; } });
16
17
  var attachment_1 = require("./types/attachment");
@@ -1,8 +1,62 @@
1
1
  import type { AttachmentFileRecord, StoredAttachment } from '../../types/attachment';
2
2
  import type { InputJsonValue } from '../../types/json-values';
3
+ import type { NotificationStatus } from '../../types/notification-status';
4
+ import type { NotificationType } from '../../types/notification-type';
3
5
  import type { AnyDatabaseNotification, AnyNotification, DatabaseNotification, DatabaseOneOffNotification, Notification, OneOffNotificationInput } from '../../types/notification';
4
6
  import type { BaseNotificationTypeConfig } from '../../types/notification-type-config';
5
7
  import type { BaseLogger } from '../loggers/base-logger';
8
+ /**
9
+ * Date range filter with optional lower and upper bounds.
10
+ */
11
+ export type DateRange = {
12
+ from?: Date;
13
+ to?: Date;
14
+ };
15
+ /**
16
+ * Flat dotted key capability map describing which filter features a backend supports.
17
+ * Use flat dotted keys for logical operators, fields, and negations:
18
+ * - `logical.*`: Operator support (and, or, not, notNested)
19
+ * - `fields.*`: Field filtering support
20
+ * - `negation.*`: Negation support for specific fields
21
+ *
22
+ * When a backend implements getFilterCapabilities(), missing keys default to true (supported),
23
+ * ensuring forward compatibility when new capabilities are added.
24
+ * Backends that don't implement getFilterCapabilities() are treated as supporting all features.
25
+ */
26
+ export type NotificationFilterCapabilities = {
27
+ [key: string]: boolean;
28
+ };
29
+ /**
30
+ * Leaf-level filter conditions for notification fields.
31
+ * All specified fields are combined with implicit AND.
32
+ */
33
+ export type NotificationFilterFields<Config extends BaseNotificationTypeConfig> = {
34
+ status?: NotificationStatus | NotificationStatus[];
35
+ notificationType?: NotificationType | NotificationType[];
36
+ adapterUsed?: string | string[];
37
+ userId?: Config['UserIdType'];
38
+ bodyTemplate?: string;
39
+ subjectTemplate?: string;
40
+ contextName?: string;
41
+ sendAfterRange?: DateRange;
42
+ createdAtRange?: DateRange;
43
+ sentAtRange?: DateRange;
44
+ };
45
+ /**
46
+ * Composable notification filter supporting logical operators.
47
+ *
48
+ * - A plain field filter applies all conditions with implicit AND.
49
+ * - `{ and: [...] }` requires all sub-filters to match.
50
+ * - `{ or: [...] }` requires at least one sub-filter to match.
51
+ * - `{ not: filter }` inverts the sub-filter.
52
+ */
53
+ export type NotificationFilter<Config extends BaseNotificationTypeConfig> = NotificationFilterFields<Config> | {
54
+ and: NotificationFilter<Config>[];
55
+ } | {
56
+ or: NotificationFilter<Config>[];
57
+ } | {
58
+ not: NotificationFilter<Config>;
59
+ };
6
60
  export interface BaseNotificationBackend<Config extends BaseNotificationTypeConfig> {
7
61
  getAllPendingNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
8
62
  getPendingNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
@@ -23,12 +77,40 @@ export interface BaseNotificationBackend<Config extends BaseNotificationTypeConf
23
77
  filterAllInAppUnreadNotifications(userId: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
24
78
  filterInAppUnreadNotifications(userId: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
25
79
  getUserEmailFromNotification(notificationId: Config['NotificationIdType']): Promise<string | undefined>;
26
- storeContextUsed(notificationId: Config['NotificationIdType'], context: InputJsonValue): Promise<void>;
80
+ storeAdapterAndContextUsed(notificationId: Config['NotificationIdType'], adapterKey: string, context: InputJsonValue): Promise<void>;
27
81
  persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
28
82
  persistOneOffNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<OneOffNotificationInput<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
29
83
  getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
30
84
  getAllOneOffNotifications(): Promise<DatabaseOneOffNotification<Config>[]>;
31
85
  getOneOffNotifications(page: number, pageSize: number): Promise<DatabaseOneOffNotification<Config>[]>;
86
+ /**
87
+ * Filter notifications using composable query filters.
88
+ * Supports filtering by status, notification type, adapter, recipient,
89
+ * body/subject templates, context, and date ranges (sendAfter, created, sent).
90
+ * Filters can be combined with logical operators (and, or, not).
91
+ *
92
+ * @param filter - Composable filter expression
93
+ * @param page - Page number (1-indexed) for pagination
94
+ * @param pageSize - Number of results per page
95
+ * @returns Matching notifications
96
+ */
97
+ filterNotifications(filter: NotificationFilter<Config>, page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
98
+ /**
99
+ * Get the filter capabilities supported by this backend.
100
+ * Returns an object with flat dotted keys indicating which filtering features are supported.
101
+ *
102
+ * Example capability names:
103
+ * - `logical.and`, `logical.or`, `logical.not`, `logical.notNested`
104
+ * - `fields.status`, `fields.notificationType`, `fields.adapterUsed`, `fields.userId`,
105
+ * `fields.bodyTemplate`, `fields.subjectTemplate`, `fields.contextName`,
106
+ * `fields.sendAfterRange`, `fields.createdAtRange`, `fields.sentAtRange`
107
+ * - `negation.sendAfterRange`, `negation.createdAtRange`, `negation.sentAtRange`
108
+ *
109
+ * If this method is not implemented, all features are assumed to be supported.
110
+ * If this method is implemented, missing keys default to true (supported) for forward compatibility.
111
+ * Only explicitly set keys to false to indicate unsupported features.
112
+ */
113
+ getFilterCapabilities?(): NotificationFilterCapabilities;
32
114
  /**
33
115
  * Inject logger into backend for debugging and monitoring
34
116
  */
@@ -73,6 +155,10 @@ export interface BaseNotificationBackend<Config extends BaseNotificationTypeConf
73
155
  */
74
156
  deleteNotificationAttachment?(notificationId: Config['NotificationIdType'], attachmentId: string): Promise<void>;
75
157
  }
158
+ /**
159
+ * Type guard to check if a filter is a field filter (leaf node).
160
+ */
161
+ export declare function isFieldFilter<Config extends BaseNotificationTypeConfig>(filter: NotificationFilter<Config>): filter is NotificationFilterFields<Config>;
76
162
  /**
77
163
  * Type guard to check if backend supports attachment operations
78
164
  */
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isFieldFilter = isFieldFilter;
3
4
  exports.supportsAttachments = supportsAttachments;
5
+ /**
6
+ * Type guard to check if a filter is a field filter (leaf node).
7
+ */
8
+ function isFieldFilter(filter) {
9
+ return !('and' in filter) && !('or' in filter) && !('not' in filter);
10
+ }
4
11
  /**
5
12
  * Type guard to check if backend supports attachment operations
6
13
  */
@@ -77,6 +77,7 @@ class VintaSend {
77
77
  this.queueService = queueService;
78
78
  }
79
79
  async send(notification) {
80
+ var _a;
80
81
  const adaptersOfType = this.adapters.filter((adapter) => adapter.notificationType === notification.notificationType);
81
82
  if (adaptersOfType.length === 0) {
82
83
  this.logger.error(`No adapter found for notification type ${notification.notificationType}`);
@@ -144,10 +145,10 @@ class VintaSend {
144
145
  this.logger.error(`Error marking notification ${notification.id} as sent: ${markSentError}`);
145
146
  }
146
147
  try {
147
- await this.backend.storeContextUsed(notification.id, context !== null && context !== void 0 ? context : {});
148
+ await this.backend.storeAdapterAndContextUsed(notification.id, (_a = adapter.key) !== null && _a !== void 0 ? _a : 'unknown', context !== null && context !== void 0 ? context : {});
148
149
  }
149
150
  catch (storeContextError) {
150
- this.logger.error(`Error storing context for notification ${notification.id}: ${storeContextError}`);
151
+ this.logger.error(`Error storing adapter and context for notification ${notification.id}: ${storeContextError}`);
151
152
  }
152
153
  }
153
154
  }
@@ -347,6 +348,7 @@ class VintaSend {
347
348
  return createdNotification;
348
349
  }
349
350
  async delayedSend(notificationId) {
351
+ var _a;
350
352
  const notification = await this.getNotification(notificationId, false);
351
353
  if (!notification) {
352
354
  this.logger.error(`Notification ${notificationId} not found`);
@@ -364,7 +366,9 @@ class VintaSend {
364
366
  return;
365
367
  }
366
368
  const context = await this.getNotificationContext(notification.contextName, notification.contextParameters);
369
+ let lastAdapterKey = 'unknown';
367
370
  for (const adapter of enqueueNotificationsAdapters) {
371
+ lastAdapterKey = (_a = adapter.key) !== null && _a !== void 0 ? _a : 'unknown';
368
372
  try {
369
373
  await adapter.send(notification, context);
370
374
  }
@@ -385,10 +389,10 @@ class VintaSend {
385
389
  }
386
390
  }
387
391
  try {
388
- await this.backend.storeContextUsed(notification.id, context);
392
+ await this.backend.storeAdapterAndContextUsed(notification.id, lastAdapterKey, context);
389
393
  }
390
394
  catch (storeContextError) {
391
- this.logger.error(`Error storing context for notification ${notification.id}: ${storeContextError}`);
395
+ this.logger.error(`Error storing adapter and context for notification ${notification.id}: ${storeContextError}`);
392
396
  }
393
397
  }
394
398
  async bulkPersistNotifications(notifications) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist"
@@ -16,6 +16,7 @@
16
16
  "test": "jest",
17
17
  "test:watch": "jest --watch",
18
18
  "test:coverage": "jest --coverage",
19
+ "test:implementations:local": "node scripts/test-implementations-local.js",
19
20
  "release:bump": "node scripts/release-bump.js",
20
21
  "release:bump:patch": "node scripts/release-bump.js --bump=patch",
21
22
  "release:bump:minor": "node scripts/release-bump.js --bump=minor",