@quatrain/messaging-firebase 1.0.1 → 1.0.3

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/LICENSE.md ADDED
@@ -0,0 +1,15 @@
1
+ # LICENSE UPDATE NOTICE
2
+
3
+ As of 01/01/2026, Quatrain Core is licensed under the **GNU Affero General Public License v3.0 (AGPL v3)**.
4
+ Previous versions remain under the MIT License.
5
+
6
+ ## Why AGPL?
7
+
8
+ We believe in open collaboration for the development ecosystem. The AGPL ensures that any modification or deployment of this BaaS stack, including over a network, benefits the entire community.
9
+
10
+ ## Commercial Services & Enterprise Usage
11
+
12
+ We provide official deployment services, technical training, and certification for Quatrain Core.
13
+ For organizations requiring a non-copyleft license (commercial license) or custom proprietary integrations, please contact the copyright holder: **Quatrain Technologies**.
14
+
15
+ Copyright © 2024-2026 Quatrain Technologies. All Rights Reserved.
@@ -1,8 +1,8 @@
1
1
  import { AbstractMessagingAdapter, MessagingParameters, MessagingRecipient, NotificationCapableAdapter, NotificationMessage } from '@quatrain/messaging';
2
2
  export declare class FirebaseMessagingAdapter extends AbstractMessagingAdapter implements NotificationCapableAdapter {
3
- private _messaging;
3
+ protected _messaging: any;
4
4
  constructor(params?: MessagingParameters);
5
- private _initializeFirebase;
5
+ protected _initializeFirebase(): void;
6
6
  /**
7
7
  * Sends a notification to a single recipient via Firebase Cloud Messaging
8
8
  *
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FirebaseMessagingAdapter = void 0;
13
13
  const messaging_1 = require("@quatrain/messaging");
14
+ const firebase_admin_1 = require("firebase-admin");
14
15
  const app_1 = require("firebase-admin/app");
15
16
  const messaging_2 = require("firebase-admin/messaging");
16
17
  class FirebaseMessagingAdapter extends messaging_1.AbstractMessagingAdapter {
@@ -24,7 +25,11 @@ class FirebaseMessagingAdapter extends messaging_1.AbstractMessagingAdapter {
24
25
  if (!this._params.config) {
25
26
  throw new Error('Firebase config is required');
26
27
  }
27
- (0, app_1.initializeApp)(this._params.config);
28
+ const serviceAccount = this._params.config;
29
+ (0, app_1.initializeApp)({
30
+ credential: firebase_admin_1.credential.cert(serviceAccount),
31
+ databaseURL: this._params.config.databaseURL,
32
+ });
28
33
  }
29
34
  this._messaging = (0, messaging_2.getMessaging)();
30
35
  }
package/package.json CHANGED
@@ -1,41 +1,40 @@
1
1
  {
2
- "name": "@quatrain/messaging-firebase",
3
- "version": "1.0.1",
4
- "license": "MIT",
5
- "main": "lib/index.js",
6
- "types": "lib/index.d.ts",
7
- "files": [
8
- "lib/",
9
- "README.md"
10
- ],
11
- "author": "Quatrain Développement SAS <developers@quatrain.com>",
12
- "peerDependencies": {
13
- "@quatrain/core": "^1.1.19"
14
- },
15
- "dependencies": {
16
- "@quatrain/messaging": "^1.0.0",
17
- "firebase-admin": "^13.0.1"
18
- },
19
- "devDependencies": {
20
- "@tsconfig/recommended": "^1.0.1",
21
- "@types/jest": "^27.0.3",
22
- "@types/node": "^22.10.1",
23
- "jest": "^27.4.7",
24
- "jest-node-exports-resolver": "^1.1.6",
25
- "jest-serial-runner": "^1.2.1",
26
- "trace-unhandled": "^2.0.1",
27
- "ts-jest": "^27.1.2",
28
- "typescript": "^5.1.5"
29
- },
30
- "scripts": {
31
- "build": "tsc",
32
- "wbuild": "tsc --watch",
33
- "test": "jest",
34
- "bump-to": "yarn version",
35
- "hash": "node ../../bin/hashFolder.js",
36
- "hash:persist": "yarn hash > .hash_latest.txt",
37
- "hash:compare": "yarn hash > .hash_newest.txt && cmp -s .hash_latest.txt .hash_newest.txt",
38
- "publish": "yarn hash:compare || yarn publish:process",
39
- "publish:process": "yarn version patch && yarn build && yarn npm publish --access public && yarn hash:persist"
40
- }
41
- }
2
+ "name": "@quatrain/messaging-firebase",
3
+ "version": "1.0.3",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "bun": "src/index.ts",
8
+ "files": [
9
+ "LICENSE.md",
10
+ "src/",
11
+ "lib/",
12
+ "README.md"
13
+ ],
14
+ "author": "Quatrain Développement SAS <developers@quatrain.com>",
15
+ "peerDependencies": {
16
+ "@quatrain/core": "^1.1.43"
17
+ },
18
+ "dependencies": {
19
+ "@quatrain/core": "^1.1.43",
20
+ "@quatrain/messaging": "^1.0.4",
21
+ "firebase-admin": "^13.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "@tsconfig/recommended": "^1.0.1",
25
+ "@types/jest": "^27.0.3",
26
+ "@types/node": "^22.10.1",
27
+ "jest": "^30.2.0",
28
+ "jest-node-exports-resolver": "^1.1.6",
29
+ "jest-serial-runner": "^1.2.1",
30
+ "trace-unhandled": "^2.0.1",
31
+ "ts-jest": "^27.1.2",
32
+ "typescript": "^5.1.5"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "wbuild": "tsc --watch",
37
+ "test": "jest",
38
+ "bump-to": "yarn version"
39
+ }
40
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ AbstractMessagingAdapter,
3
+ Messaging,
4
+ MessagingParameters,
5
+ MessagingRecipient,
6
+ NotificationCapableAdapter,
7
+ NotificationMessage,
8
+ } from '@quatrain/messaging'
9
+ import { credential } from 'firebase-admin'
10
+ import { getApps, initializeApp, ServiceAccount } from 'firebase-admin/app'
11
+ import { getMessaging, Message } from 'firebase-admin/messaging'
12
+
13
+ export class FirebaseMessagingAdapter
14
+ extends AbstractMessagingAdapter
15
+ implements NotificationCapableAdapter
16
+ {
17
+ protected _messaging: any
18
+
19
+ constructor(params: MessagingParameters = {}) {
20
+ super(params)
21
+ this._initializeFirebase()
22
+ }
23
+
24
+ protected _initializeFirebase() {
25
+ try {
26
+ if (getApps().length === 0) {
27
+ if (!this._params.config) {
28
+ throw new Error('Firebase config is required')
29
+ }
30
+ const serviceAccount: ServiceAccount = this._params.config
31
+ initializeApp({
32
+ credential: credential.cert(serviceAccount),
33
+ databaseURL: this._params.config.databaseURL,
34
+ })
35
+ }
36
+ this._messaging = getMessaging()
37
+ } catch (error) {
38
+ Messaging.error(
39
+ `Failed to initialize Firebase: ${(error as Error).message}`
40
+ )
41
+ throw error
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Sends a notification to a single recipient via Firebase Cloud Messaging
47
+ *
48
+ * @param recipient - The message recipient containing contact info and FCM token
49
+ * @param message - The notification message with title, body, and optional data
50
+ * @returns Promise<string> - The Firebase message ID on successful delivery
51
+ * @throws Error if recipient has no message token or Firebase send fails
52
+ */
53
+ async sendNotification(
54
+ recipient: MessagingRecipient,
55
+ message: NotificationMessage
56
+ ): Promise<string> {
57
+ try {
58
+ if (!recipient.messageToken) {
59
+ throw new Error('Recipient message token is required')
60
+ }
61
+
62
+ const firebaseMessage: Message = {
63
+ token: message.token || recipient.messageToken,
64
+ notification: {
65
+ title: message.title,
66
+ body: message.body || '',
67
+ },
68
+ data: this._serializeData(message.data || {}),
69
+ }
70
+
71
+ const response = await this._messaging.send(firebaseMessage)
72
+
73
+ Messaging.info(
74
+ `Notification sent successfully to ${recipient.firstname} ${recipient.lastname}: ${response}`
75
+ )
76
+
77
+ return response
78
+ } catch (error) {
79
+ const errorMessage = `Failed to send notification to ${
80
+ recipient.firstname
81
+ } ${recipient.lastname}: ${(error as Error).message}`
82
+ Messaging.error(errorMessage)
83
+ throw new Error(errorMessage)
84
+ }
85
+ }
86
+
87
+ async sendNotifications(
88
+ recipients: MessagingRecipient[],
89
+ message: NotificationMessage
90
+ ): Promise<{
91
+ successCount: number
92
+ failureCount: number
93
+ responses: any[]
94
+ }> {
95
+ try {
96
+ const tokens = recipients
97
+ .map((recipient) => recipient.messageToken)
98
+ .filter((token) => token) as string[]
99
+
100
+ if (tokens.length === 0) {
101
+ throw new Error('No valid message tokens found')
102
+ }
103
+
104
+ const multicastMessage = {
105
+ tokens,
106
+ notification: {
107
+ title: message.title,
108
+ body: message.body || '',
109
+ },
110
+ data: this._serializeData(message.data || {}),
111
+ }
112
+
113
+ const response = await this._messaging.sendMulticast(multicastMessage)
114
+
115
+ Messaging.info(
116
+ `Multicast notification sent: ${response.successCount} successful, ${response.failureCount} failed`
117
+ )
118
+
119
+ return response
120
+ } catch (error) {
121
+ const errorMessage = `Failed to send multicast notification: ${
122
+ (error as Error).message
123
+ }`
124
+ Messaging.error(errorMessage)
125
+ throw new Error(errorMessage)
126
+ }
127
+ }
128
+
129
+ private _serializeData(data: any): Record<string, string> {
130
+ const serialized: Record<string, string> = {}
131
+
132
+ for (const [key, value] of Object.entries(data)) {
133
+ if (typeof value === 'string') {
134
+ serialized[key] = value
135
+ } else {
136
+ serialized[key] = JSON.stringify(value)
137
+ }
138
+ }
139
+
140
+ return serialized
141
+ }
142
+ }
@@ -0,0 +1,175 @@
1
+ import { FirebaseMessagingAdapter } from '../FirebaseMessagingAdapter'
2
+ import { MessagingRecipient, NotificationMessage } from '@quatrain/messaging'
3
+
4
+ // Mock Firebase Admin SDK
5
+ jest.mock('firebase-admin/app', () => ({
6
+ getApps: jest.fn(() => []),
7
+ initializeApp: jest.fn()
8
+ }))
9
+
10
+ jest.mock('firebase-admin/messaging', () => ({
11
+ getMessaging: jest.fn(() => ({
12
+ send: jest.fn().mockResolvedValue('message-id-123'),
13
+ sendMulticast: jest.fn().mockResolvedValue({
14
+ successCount: 2,
15
+ failureCount: 0,
16
+ responses: [
17
+ { success: true, messageId: 'msg-1' },
18
+ { success: true, messageId: 'msg-2' }
19
+ ]
20
+ })
21
+ }))
22
+ }))
23
+
24
+ describe('FirebaseMessagingAdapter', () => {
25
+ let adapter: FirebaseMessagingAdapter
26
+ const mockConfig = {
27
+ projectId: 'test-project',
28
+ privateKey: 'mock-private-key',
29
+ clientEmail: 'test@test.com'
30
+ }
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks()
34
+ adapter = new FirebaseMessagingAdapter({
35
+ config: mockConfig
36
+ })
37
+ })
38
+
39
+ describe('constructor', () => {
40
+ test('should initialize with config', () => {
41
+ expect(adapter).toBeInstanceOf(FirebaseMessagingAdapter)
42
+ })
43
+
44
+ test('should throw error without config', () => {
45
+ expect(() => {
46
+ new FirebaseMessagingAdapter({})
47
+ }).toThrow('Firebase config is required')
48
+ })
49
+ })
50
+
51
+ describe('sendNotification', () => {
52
+ const mockRecipient: MessagingRecipient = {
53
+ firstname: 'John',
54
+ lastname: 'Doe',
55
+ email: 'john@example.com',
56
+ messageToken: 'mock-token-123'
57
+ }
58
+
59
+ const mockMessage: NotificationMessage = {
60
+ title: 'Test Notification',
61
+ body: 'This is a test message',
62
+ data: { userId: '123', action: 'test' }
63
+ }
64
+
65
+ test('should send notification successfully', async () => {
66
+ const result = await adapter.sendNotification(mockRecipient, mockMessage)
67
+ expect(result).toBe('message-id-123')
68
+ })
69
+
70
+ test('should throw error when recipient has no message token', async () => {
71
+ const recipientWithoutToken = { ...mockRecipient, messageToken: undefined }
72
+
73
+ await expect(
74
+ adapter.sendNotification(recipientWithoutToken, mockMessage)
75
+ ).rejects.toThrow('Recipient message token is required')
76
+ })
77
+
78
+ test('should use message token from message if provided', async () => {
79
+ const messageWithToken = { ...mockMessage, token: 'custom-token' }
80
+
81
+ const result = await adapter.sendNotification(mockRecipient, messageWithToken)
82
+ expect(result).toBe('message-id-123')
83
+ })
84
+
85
+ test('should handle empty body gracefully', async () => {
86
+ const messageWithoutBody = { ...mockMessage, body: undefined }
87
+
88
+ const result = await adapter.sendNotification(mockRecipient, messageWithoutBody)
89
+ expect(result).toBe('message-id-123')
90
+ })
91
+ })
92
+
93
+ describe('sendMulticast', () => {
94
+ const mockRecipients: MessagingRecipient[] = [
95
+ {
96
+ firstname: 'John',
97
+ lastname: 'Doe',
98
+ email: 'john@example.com',
99
+ messageToken: 'token-1'
100
+ },
101
+ {
102
+ firstname: 'Jane',
103
+ lastname: 'Smith',
104
+ email: 'jane@example.com',
105
+ messageToken: 'token-2'
106
+ }
107
+ ]
108
+
109
+ const mockMessage: NotificationMessage = {
110
+ title: 'Multicast Test',
111
+ body: 'This is a multicast message'
112
+ }
113
+
114
+ test('should send multicast notification successfully', async () => {
115
+ const result = await adapter.sendMulticast(mockRecipients, mockMessage)
116
+
117
+ expect(result.successCount).toBe(2)
118
+ expect(result.failureCount).toBe(0)
119
+ expect(result.responses).toHaveLength(2)
120
+ })
121
+
122
+ test('should filter out recipients without tokens', async () => {
123
+ const recipientsWithMissingTokens = [
124
+ ...mockRecipients,
125
+ {
126
+ firstname: 'Bob',
127
+ lastname: 'Wilson',
128
+ email: 'bob@example.com',
129
+ messageToken: undefined
130
+ }
131
+ ]
132
+
133
+ const result = await adapter.sendMulticast(recipientsWithMissingTokens, mockMessage)
134
+ expect(result.successCount).toBe(2)
135
+ })
136
+
137
+ test('should throw error when no valid tokens found', async () => {
138
+ const recipientsWithoutTokens = mockRecipients.map(r => ({
139
+ ...r,
140
+ messageToken: undefined
141
+ }))
142
+
143
+ await expect(
144
+ adapter.sendMulticast(recipientsWithoutTokens, mockMessage)
145
+ ).rejects.toThrow('No valid message tokens found')
146
+ })
147
+ })
148
+
149
+ describe('_serializeData', () => {
150
+ test('should serialize complex data correctly', () => {
151
+ const adapter = new FirebaseMessagingAdapter({ config: mockConfig })
152
+ const testData = {
153
+ stringValue: 'test',
154
+ numberValue: 123,
155
+ booleanValue: true,
156
+ objectValue: { nested: 'object' },
157
+ arrayValue: [1, 2, 3]
158
+ }
159
+
160
+ const result = (adapter as any)._serializeData(testData)
161
+
162
+ expect(result.stringValue).toBe('test')
163
+ expect(result.numberValue).toBe('123')
164
+ expect(result.booleanValue).toBe('true')
165
+ expect(result.objectValue).toBe('{"nested":"object"}')
166
+ expect(result.arrayValue).toBe('[1,2,3]')
167
+ })
168
+
169
+ test('should handle empty data', () => {
170
+ const adapter = new FirebaseMessagingAdapter({ config: mockConfig })
171
+ const result = (adapter as any)._serializeData({})
172
+ expect(result).toEqual({})
173
+ })
174
+ })
175
+ })
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { FirebaseMessagingAdapter } from './FirebaseMessagingAdapter'
2
+
3
+ export { FirebaseMessagingAdapter }