@things-factory/notification 10.1.250 → 10.1.255
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/fcm.d.ts +6 -10
- package/dist-server/controllers/fcm.js +73 -122
- package/dist-server/controllers/fcm.js.map +1 -1
- package/dist-server/controllers/notice/deliver-notice.d.ts +1 -1
- package/dist-server/controllers/notice/deliver-notice.js.map +1 -1
- package/dist-server/middlewares/notification-middleware.d.ts +1 -0
- package/dist-server/middlewares/notification-middleware.js +11 -1
- package/dist-server/middlewares/notification-middleware.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +4 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/notification-token/index.d.ts +2 -0
- package/dist-server/service/notification-token/index.js +6 -0
- package/dist-server/service/notification-token/index.js.map +1 -0
- package/dist-server/service/notification-token/notification-token.d.ts +22 -0
- package/dist-server/service/notification-token/notification-token.js +64 -0
- package/dist-server/service/notification-token/notification-token.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/tests/push-tokens.test.ts +190 -0
|
@@ -15,19 +15,15 @@ export declare function sendNotification({ receivers, message }: {
|
|
|
15
15
|
receivers: any;
|
|
16
16
|
message: any;
|
|
17
17
|
}): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Keeps the browser's FCM token for the signed-in person. A token held by someone else moves to them - one browser
|
|
20
|
+
* reaches the person signed in on it.
|
|
21
|
+
*/
|
|
18
22
|
export declare function register(user: any, { subscription }: {
|
|
19
23
|
subscription: any;
|
|
20
24
|
}): Promise<boolean>;
|
|
25
|
+
/** Forgets the browser's token, when the person who turns notices off is the one holding it. */
|
|
21
26
|
export declare function unregister(user: any, { subscription }: {
|
|
22
27
|
subscription: any;
|
|
23
28
|
}): Promise<boolean>;
|
|
24
|
-
export declare function notify({ receivers, tokens, topic, title, body, data, image, actions }:
|
|
25
|
-
receivers: any;
|
|
26
|
-
tokens: any;
|
|
27
|
-
topic: any;
|
|
28
|
-
title: any;
|
|
29
|
-
body: any;
|
|
30
|
-
data: any;
|
|
31
|
-
image: any;
|
|
32
|
-
actions: any;
|
|
33
|
-
}): Promise<void>;
|
|
29
|
+
export declare function notify({ receivers, tokens, topic, title, body, data, image, actions }: any): Promise<void>;
|
|
@@ -5,14 +5,15 @@ exports.sendNotification = sendNotification;
|
|
|
5
5
|
exports.register = register;
|
|
6
6
|
exports.unregister = unregister;
|
|
7
7
|
exports.notify = notify;
|
|
8
|
-
const
|
|
9
|
-
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
|
|
8
|
+
const typeorm_1 = require("typeorm");
|
|
10
9
|
const env_1 = require("@things-factory/env");
|
|
10
|
+
const shell_1 = require("@things-factory/shell");
|
|
11
11
|
const app_1 = require("firebase-admin/app");
|
|
12
12
|
const messaging_1 = require("firebase-admin/messaging");
|
|
13
|
+
const notification_token_js_1 = require("../service/notification-token/notification-token.js");
|
|
13
14
|
const notificationConfig = env_1.config.get('notification') || {};
|
|
14
15
|
const { fcm, vapidKey } = notificationConfig;
|
|
15
|
-
const { serviceAccount, appConfig
|
|
16
|
+
const { serviceAccount, appConfig } = fcm || {};
|
|
16
17
|
const { publicKey } = vapidKey || {};
|
|
17
18
|
var messaging;
|
|
18
19
|
if (serviceAccount) {
|
|
@@ -27,6 +28,10 @@ if (serviceAccount) {
|
|
|
27
28
|
env_1.logger.error(err);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
/* What FCM answers for a token that will never be reached again - the browser unsubscribed, or the token was never one. */
|
|
32
|
+
const GONE_TOKEN_ERRORS = ['messaging/registration-token-not-registered', 'messaging/invalid-registration-token'];
|
|
33
|
+
/* sendEachForMulticast takes at most this many tokens a call. */
|
|
34
|
+
const MULTICAST_LIMIT = 500;
|
|
30
35
|
function getConfig() {
|
|
31
36
|
return messaging
|
|
32
37
|
? {
|
|
@@ -50,154 +55,100 @@ async function sendNotification({ receivers, message }) {
|
|
|
50
55
|
...message
|
|
51
56
|
});
|
|
52
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Keeps the browser's FCM token for the signed-in person. A token held by someone else moves to them - one browser
|
|
60
|
+
* reaches the person signed in on it.
|
|
61
|
+
*/
|
|
53
62
|
async function register(user, { subscription }) {
|
|
54
|
-
if (!
|
|
63
|
+
if (!subscription || !user) {
|
|
55
64
|
return false;
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
operation: 'create',
|
|
62
|
-
notification_key_name: user.id,
|
|
63
|
-
registration_ids: [subscription]
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
await postDeviceGroup({
|
|
68
|
-
operation: 'add',
|
|
69
|
-
notification_key_name: user.id,
|
|
70
|
-
notification_key,
|
|
71
|
-
registration_ids: [subscription]
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
66
|
+
const repository = (0, shell_1.getRepository)(notification_token_js_1.NotificationToken);
|
|
67
|
+
const held = await repository.findOne({ where: { token: subscription } });
|
|
68
|
+
if (!held) {
|
|
69
|
+
await repository.save({ token: subscription, user, creator: user, updater: user });
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
else if (held.userId !== user.id) {
|
|
72
|
+
await repository.save({ ...held, user, updater: user });
|
|
78
73
|
}
|
|
74
|
+
return true;
|
|
79
75
|
}
|
|
76
|
+
/** Forgets the browser's token, when the person who turns notices off is the one holding it. */
|
|
80
77
|
async function unregister(user, { subscription }) {
|
|
81
|
-
if (!
|
|
78
|
+
if (!subscription || !user) {
|
|
82
79
|
return false;
|
|
83
80
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
operation: 'remove',
|
|
89
|
-
notification_key_name: user.id,
|
|
90
|
-
notification_key,
|
|
91
|
-
registration_ids: [subscription]
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
return;
|
|
81
|
+
const repository = (0, shell_1.getRepository)(notification_token_js_1.NotificationToken);
|
|
82
|
+
const held = await repository.findOne({ where: { token: subscription } });
|
|
83
|
+
if (held && held.userId === user.id) {
|
|
84
|
+
await repository.delete(held.id);
|
|
98
85
|
}
|
|
86
|
+
return true;
|
|
99
87
|
}
|
|
100
88
|
/*
|
|
101
|
-
* `receivers` are user ids
|
|
89
|
+
* `receivers` are user ids; each is reached at every browser that registered a token for them. `tokens` are
|
|
90
|
+
* registration tokens named directly. There was a `privileges` parameter until 2026-10-02; it was never read, so a
|
|
102
91
|
* caller that named privileges instead of people reached nobody and was not told. No caller used it.
|
|
92
|
+
*
|
|
93
|
+
* Until 2026-11-05 a receiver was reached through a legacy FCM device group, keyed by a server key that no config set
|
|
94
|
+
* and that Google shut down in 2024 - web push reached no one.
|
|
103
95
|
*/
|
|
104
96
|
async function notify({ receivers, tokens, topic, title, body, data, image, actions }) {
|
|
105
97
|
if (!messaging) {
|
|
106
98
|
return;
|
|
107
99
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
const message = {
|
|
101
|
+
notification: {
|
|
102
|
+
title: title || '',
|
|
103
|
+
body: body || '',
|
|
104
|
+
...(image ? { imageUrl: image } : {})
|
|
105
|
+
},
|
|
106
|
+
data: stringValues(data),
|
|
107
|
+
/* a phone asleep holds back pushes that are not urgent */
|
|
108
|
+
webpush: { headers: { Urgency: 'high' } }
|
|
113
109
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
const owned = receivers instanceof Array && receivers.length > 0
|
|
111
|
+
? await (0, shell_1.getRepository)(notification_token_js_1.NotificationToken).find({ where: { user: { id: (0, typeorm_1.In)(receivers) } } })
|
|
112
|
+
: [];
|
|
113
|
+
const named = tokens instanceof Array ? tokens.filter(Boolean) : [];
|
|
114
|
+
const all = [...new Set([...named, ...owned.map(owned => owned.token)])];
|
|
115
|
+
const gone = [];
|
|
116
|
+
for (let start = 0; start < all.length; start += MULTICAST_LIMIT) {
|
|
117
|
+
const batch = all.slice(start, start + MULTICAST_LIMIT);
|
|
118
|
+
try {
|
|
119
|
+
const response = await messaging.sendEachForMulticast({ ...message, tokens: batch });
|
|
120
|
+
response.responses.forEach((sent, index) => {
|
|
121
|
+
if (sent.success)
|
|
122
|
+
return;
|
|
123
|
+
if (GONE_TOKEN_ERRORS.includes(sent.error?.code)) {
|
|
124
|
+
gone.push(batch[index]);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
env_1.logger.error(`[notification] push to a token failed: ${sent.error?.code} ${sent.error?.message}`);
|
|
128
|
+
}
|
|
120
129
|
});
|
|
121
|
-
if (response.failureCount > 0) {
|
|
122
|
-
const failedTokens = [];
|
|
123
|
-
response.responses.forEach((resp, idx) => {
|
|
124
|
-
if (!resp.success) {
|
|
125
|
-
failedTokens.push(tokens[idx]);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
env_1.logger.error('List of tokens that caused failures: ' + failedTokens);
|
|
129
|
-
}
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const response = await messaging.send({
|
|
135
|
-
notification,
|
|
136
|
-
data,
|
|
137
|
-
token
|
|
138
|
-
});
|
|
139
|
-
env_1.logger.log('Successfully sent message:', response);
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
env_1.logger.error('Error sending message:', err);
|
|
143
|
-
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
env_1.logger.error('[notification] push failed', err);
|
|
144
133
|
}
|
|
145
134
|
}
|
|
135
|
+
const goneOwned = owned.filter(owned => gone.includes(owned.token));
|
|
136
|
+
if (goneOwned.length > 0) {
|
|
137
|
+
await (0, shell_1.getRepository)(notification_token_js_1.NotificationToken).delete(goneOwned.map(owned => owned.id));
|
|
138
|
+
}
|
|
146
139
|
if (topic) {
|
|
147
140
|
try {
|
|
148
|
-
|
|
149
|
-
notification,
|
|
150
|
-
data,
|
|
151
|
-
topic
|
|
152
|
-
});
|
|
153
|
-
env_1.logger.log('Successfully sent message:', response);
|
|
141
|
+
await messaging.send({ ...message, topic });
|
|
154
142
|
}
|
|
155
143
|
catch (err) {
|
|
156
|
-
env_1.logger.error('
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (receivers && receivers instanceof Array) {
|
|
160
|
-
for (let receiver of receivers) {
|
|
161
|
-
const notification_key = await getDeviceGroup(receiver);
|
|
162
|
-
if (notification_key) {
|
|
163
|
-
await messaging.sendToDeviceGroup(notification_key, {
|
|
164
|
-
notification,
|
|
165
|
-
data
|
|
166
|
-
});
|
|
167
|
-
}
|
|
144
|
+
env_1.logger.error('[notification] push to a topic failed', err);
|
|
168
145
|
}
|
|
169
146
|
}
|
|
170
147
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Authorization: `key=${serverKey}`,
|
|
177
|
-
project_id: appConfig.messagingSenderId
|
|
178
|
-
},
|
|
179
|
-
body: JSON.stringify(body)
|
|
180
|
-
});
|
|
181
|
-
if (response.ok) {
|
|
182
|
-
const { notification_key } = await response.json();
|
|
183
|
-
return notification_key;
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
console.error('postDeviceGroup-notok', response.status, await response.text());
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async function getDeviceGroup(notificationKeyName) {
|
|
190
|
-
const response = await (0, node_fetch_1.default)(`https://fcm.googleapis.com/fcm/notification?notification_key_name=${notificationKeyName}`, {
|
|
191
|
-
method: 'GET',
|
|
192
|
-
headers: {
|
|
193
|
-
'Content-Type': 'application/json',
|
|
194
|
-
Authorization: `key=${serverKey}`,
|
|
195
|
-
project_id: appConfig.messagingSenderId
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
if (response.ok) {
|
|
199
|
-
const { notification_key } = await response.json();
|
|
200
|
-
return notification_key;
|
|
201
|
-
}
|
|
148
|
+
/* FCM takes only strings in data; a value left out is not sent. */
|
|
149
|
+
function stringValues(data) {
|
|
150
|
+
return Object.fromEntries(Object.entries(data || {})
|
|
151
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
152
|
+
.map(([key, value]) => [key, String(value)]));
|
|
202
153
|
}
|
|
203
154
|
//# sourceMappingURL=fcm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fcm.js","sourceRoot":"","sources":["../../server/controllers/fcm.ts"],"names":[],"mappings":"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","sourcesContent":["import fetch from 'node-fetch'\n\nimport { config, logger } from '@things-factory/env'\n\nimport { initializeApp, cert } from 'firebase-admin/app'\nimport { getMessaging } from 'firebase-admin/messaging'\n\nconst notificationConfig = config.get('notification') || {}\nconst { fcm, vapidKey } = notificationConfig\nconst { serviceAccount, appConfig, serverKey } = fcm || {}\nconst { publicKey } = vapidKey || {}\n\nvar messaging\n\nif (serviceAccount) {\n try {\n const app = initializeApp({\n credential: cert(serviceAccount)\n })\n\n messaging = getMessaging(app)\n } catch (err) {\n logger.error('incorrect notification configuration')\n logger.error(err)\n }\n}\n\nexport function getConfig() {\n return messaging\n ? {\n appConfig,\n vapidPublicKey: publicKey\n }\n : {}\n}\n\n/**\n * sendNotification\n *\n * @param receiver user.id who will receive notification\n * @param message message object to be sent\n */\nexport async function sendNotification({ receivers, message }) {\n if (!messaging) {\n return\n }\n\n notify({\n receivers,\n ...message\n })\n}\n\nexport async function register(user, { subscription }) {\n if (!messaging || !subscription || !user) {\n return false\n }\n\n try {\n var notification_key = await getDeviceGroup(user.id)\n\n if (!notification_key) {\n await postDeviceGroup({\n operation: 'create',\n notification_key_name: user.id,\n registration_ids: [subscription]\n })\n } else {\n await postDeviceGroup({\n operation: 'add',\n notification_key_name: user.id,\n notification_key,\n registration_ids: [subscription]\n })\n }\n\n return true\n } catch (err) {\n return\n }\n}\n\nexport async function unregister(user, { subscription }) {\n if (!messaging || !subscription || !user) {\n return false\n }\n\n try {\n var notification_key = await getDeviceGroup(user.id)\n\n if (notification_key) {\n await postDeviceGroup({\n operation: 'remove',\n notification_key_name: user.id,\n notification_key,\n registration_ids: [subscription]\n })\n }\n\n return true\n } catch (err) {\n return\n }\n}\n\n/*\n * `receivers` are user ids. There was a `privileges` parameter until 2026-10-02; it was never read, so a\n * caller that named privileges instead of people reached nobody and was not told. No caller used it.\n */\nexport async function notify({ receivers, tokens, topic, title, body, data, image, actions }) {\n if (!messaging) {\n return\n }\n\n // Caution: non-string attributes are not allowed in FCM payload validation\n var notification = {\n title: title || '',\n body: body || '',\n image: image || ''\n }\n\n if (tokens && tokens instanceof Array && tokens.length > 0) {\n if (tokens.length > 1) {\n const response = await messaging.sendMulticast({\n notification,\n data,\n tokens\n })\n\n if (response.failureCount > 0) {\n const failedTokens = []\n response.responses.forEach((resp, idx) => {\n if (!resp.success) {\n failedTokens.push(tokens[idx])\n }\n })\n logger.error('List of tokens that caused failures: ' + failedTokens)\n }\n } else {\n const token = tokens[0]\n\n try {\n const response = await messaging.send({\n notification,\n data,\n token\n })\n logger.log('Successfully sent message:', response)\n } catch (err) {\n logger.error('Error sending message:', err)\n }\n }\n }\n\n if (topic) {\n try {\n const response = await messaging.send({\n notification,\n data,\n topic\n })\n logger.log('Successfully sent message:', response)\n } catch (err) {\n logger.error('Error sending message:', err)\n }\n }\n\n if (receivers && receivers instanceof Array) {\n for (let receiver of receivers) {\n const notification_key = await getDeviceGroup(receiver)\n\n if (notification_key) {\n await messaging.sendToDeviceGroup(notification_key, {\n notification,\n data\n })\n }\n }\n }\n}\n\nasync function postDeviceGroup(body) {\n var response = await fetch('https://fcm.googleapis.com/fcm/notification', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `key=${serverKey}`,\n project_id: appConfig.messagingSenderId\n },\n body: JSON.stringify(body)\n })\n\n if (response.ok) {\n const { notification_key } = await response.json()\n return notification_key\n } else {\n console.error('postDeviceGroup-notok', response.status, await response.text())\n }\n}\n\nasync function getDeviceGroup(notificationKeyName) {\n const response = await fetch(\n `https://fcm.googleapis.com/fcm/notification?notification_key_name=${notificationKeyName}`,\n {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `key=${serverKey}`,\n project_id: appConfig.messagingSenderId\n }\n }\n )\n\n if (response.ok) {\n const { notification_key } = await response.json()\n return notification_key\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fcm.js","sourceRoot":"","sources":["../../server/controllers/fcm.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { initializeApp, cert } from 'firebase-admin/app'\nimport { getMessaging, type Messaging } from 'firebase-admin/messaging'\n\nimport { NotificationToken } from '../service/notification-token/notification-token.js'\n\nconst notificationConfig = config.get('notification') || {}\nconst { fcm, vapidKey } = notificationConfig\nconst { serviceAccount, appConfig } = fcm || {}\nconst { publicKey } = vapidKey || {}\n\nvar messaging: Messaging | undefined\n\nif (serviceAccount) {\n try {\n const app = initializeApp({\n credential: cert(serviceAccount)\n })\n\n messaging = getMessaging(app)\n } catch (err) {\n logger.error('incorrect notification configuration')\n logger.error(err)\n }\n}\n\n/* What FCM answers for a token that will never be reached again - the browser unsubscribed, or the token was never one. */\nconst GONE_TOKEN_ERRORS = ['messaging/registration-token-not-registered', 'messaging/invalid-registration-token']\n\n/* sendEachForMulticast takes at most this many tokens a call. */\nconst MULTICAST_LIMIT = 500\n\nexport function getConfig() {\n return messaging\n ? {\n appConfig,\n vapidPublicKey: publicKey\n }\n : {}\n}\n\n/**\n * sendNotification\n *\n * @param receiver user.id who will receive notification\n * @param message message object to be sent\n */\nexport async function sendNotification({ receivers, message }) {\n if (!messaging) {\n return\n }\n\n notify({\n receivers,\n ...message\n })\n}\n\n/**\n * Keeps the browser's FCM token for the signed-in person. A token held by someone else moves to them - one browser\n * reaches the person signed in on it.\n */\nexport async function register(user, { subscription }) {\n if (!subscription || !user) {\n return false\n }\n\n const repository = getRepository(NotificationToken)\n const held = await repository.findOne({ where: { token: subscription } })\n\n if (!held) {\n await repository.save({ token: subscription, user, creator: user, updater: user })\n } else if (held.userId !== user.id) {\n await repository.save({ ...held, user, updater: user })\n }\n\n return true\n}\n\n/** Forgets the browser's token, when the person who turns notices off is the one holding it. */\nexport async function unregister(user, { subscription }) {\n if (!subscription || !user) {\n return false\n }\n\n const repository = getRepository(NotificationToken)\n const held = await repository.findOne({ where: { token: subscription } })\n\n if (held && held.userId === user.id) {\n await repository.delete(held.id)\n }\n\n return true\n}\n\n/*\n * `receivers` are user ids; each is reached at every browser that registered a token for them. `tokens` are\n * registration tokens named directly. There was a `privileges` parameter until 2026-10-02; it was never read, so a\n * caller that named privileges instead of people reached nobody and was not told. No caller used it.\n *\n * Until 2026-11-05 a receiver was reached through a legacy FCM device group, keyed by a server key that no config set\n * and that Google shut down in 2024 - web push reached no one.\n */\nexport async function notify({ receivers, tokens, topic, title, body, data, image, actions }: any) {\n if (!messaging) {\n return\n }\n\n const message = {\n notification: {\n title: title || '',\n body: body || '',\n ...(image ? { imageUrl: image } : {})\n },\n data: stringValues(data),\n /* a phone asleep holds back pushes that are not urgent */\n webpush: { headers: { Urgency: 'high' } }\n }\n\n const owned: NotificationToken[] =\n receivers instanceof Array && receivers.length > 0\n ? await getRepository(NotificationToken).find({ where: { user: { id: In(receivers) } } })\n : []\n\n const named = tokens instanceof Array ? tokens.filter(Boolean) : []\n const all = [...new Set([...named, ...owned.map(owned => owned.token)])]\n\n const gone: string[] = []\n for (let start = 0; start < all.length; start += MULTICAST_LIMIT) {\n const batch = all.slice(start, start + MULTICAST_LIMIT)\n\n try {\n const response = await messaging.sendEachForMulticast({ ...message, tokens: batch })\n\n response.responses.forEach((sent, index) => {\n if (sent.success) return\n if (GONE_TOKEN_ERRORS.includes(sent.error?.code)) {\n gone.push(batch[index])\n } else {\n logger.error(`[notification] push to a token failed: ${sent.error?.code} ${sent.error?.message}`)\n }\n })\n } catch (err) {\n logger.error('[notification] push failed', err)\n }\n }\n\n const goneOwned = owned.filter(owned => gone.includes(owned.token))\n if (goneOwned.length > 0) {\n await getRepository(NotificationToken).delete(goneOwned.map(owned => owned.id))\n }\n\n if (topic) {\n try {\n await messaging.send({ ...message, topic })\n } catch (err) {\n logger.error('[notification] push to a topic failed', err)\n }\n }\n}\n\n/* FCM takes only strings in data; a value left out is not sent. */\nfunction stringValues(data?: Record<string, unknown>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(data || {})\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) => [key, String(value)])\n )\n}\n"]}
|
|
@@ -15,7 +15,7 @@ export type Notice = {
|
|
|
15
15
|
level?: 'info' | 'warn';
|
|
16
16
|
receivers: NoticeReceiver[];
|
|
17
17
|
words: (lng: string) => NoticeWords;
|
|
18
|
-
/** The page, relative to the domain - what the in-app notice
|
|
18
|
+
/** The page, relative to the domain - what the in-app notice and the web push open. */
|
|
19
19
|
address: string;
|
|
20
20
|
/** The same page as a full address for mail and Teams; absent when no request said where we are. */
|
|
21
21
|
link?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, mailForm, type AppBrand } from '@things-factory/email-base'\n\nimport { noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice opens. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n /** The application whose name, tagline and colour the mail carries; absent, the default. */\n brand?: AppBrand\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: mailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
|
|
1
|
+
{"version":3,"file":"deliver-notice.js","sourceRoot":"","sources":["../../../server/controllers/notice/deliver-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Sending one notice down each channel (ADR-0164). The channels come in as functions, so a test hands in fakes and\n * sees what each one was given; send-notice.ts hands in the real ones.\n */\nimport { languageOf, mailForm, type AppBrand } from '@things-factory/email-base'\n\nimport { noticeChannels, teamsMessage, type NoticeWords } from './notice-channels.js'\n\nexport type NoticeReceiver = { id: string; email?: string | null; locale?: string | null }\n\n/**\n * A notice, settled by the module whose event it tells of - who, what and where. The words are asked per language,\n * so each person reads their own.\n */\nexport type Notice = {\n /** What kind of thing it tells of - `approval`, `announcement`. The in-app notice carries it. */\n type: string\n level?: 'info' | 'warn'\n receivers: NoticeReceiver[]\n words: (lng: string) => NoticeWords\n /** The page, relative to the domain - what the in-app notice and the web push open. */\n address: string\n /** The same page as a full address for mail and Teams; absent when no request said where we are. */\n link?: string\n /** The language of a person who has not chosen one. */\n defaultLanguage?: string\n /** Names the notice in the log. */\n label: string\n /** How `words().body` is written. Markdown is rendered in the mail; Teams and push take it as text. */\n format?: 'text' | 'markdown'\n /**\n * The channels this notice may take beyond the app, when its sender chooses per notice (an announcement's\n * \"send by mail too\"). Left out, it takes every channel the domain turned on; a channel the domain has not turned on\n * is never taken.\n */\n channels?: { email?: boolean; teams?: boolean }\n /**\n * Said to everyone in the domain (an announcement to the whole domain). The app, push and mail still go to each\n * person; Teams posts once to the domain's channel webhook, in the default language. Only a broadcast goes to Teams -\n * a notice to some people never lands where everyone reads.\n */\n broadcast?: boolean\n}\n\n/** What went out - mail counted per person, the Teams channel post once. A sender that keeps a record stores this. */\nexport type NoticeResult = {\n receivers: number\n email: { sent: number; failed: number }\n teams: { sent: number; failed: number }\n}\n\nexport type NoticeChannelFunctions = {\n /** The domain's attributes (EnvVar) - read when the notice goes. */\n attributes: () => Promise<Record<string, string | undefined>>\n /** notification's `notify` for the domain; absent, nothing goes in the app. */\n push?: (payload: any) => unknown\n /** email-base's `sendEmail` - answers the transport's receipt, nothing when it failed. */\n mail: (mail: { receiver: string; subject: string; content: string }) => Promise<unknown>\n /** Posts JSON to the Teams webhook and answers whether it took it. */\n teams: (url: string, body: unknown) => Promise<{ ok: boolean; status?: number }>\n report: (message: string, error?: unknown) => void\n /** The application whose name, tagline and colour the mail carries; absent, the default. */\n brand?: AppBrand\n}\n\n/** The domain's own address - the notice's full link without its page. */\nfunction baseOf(notice: Notice): string | undefined {\n return notice.link && notice.link.endsWith(notice.address) ? notice.link.slice(0, notice.link.length - notice.address.length) : undefined\n}\n\n/**\n * Tells each receiver in the app and by web push always, and by mail when the domain turned it on; a broadcast is\n * also posted once to the domain's Teams channel. A channel that fails is reported with its name and the others still\n * go - nothing here throws, because what it tells of has already happened.\n */\nexport async function deliverNotice(notice: Notice, channels: NoticeChannelFunctions): Promise<NoticeResult> {\n const result: NoticeResult = { receivers: notice.receivers.length, email: { sent: 0, failed: 0 }, teams: { sent: 0, failed: 0 } }\n if (!notice.receivers.length) return result\n\n let turnedOn: ReturnType<typeof noticeChannels> = { email: false }\n try {\n turnedOn = noticeChannels(await channels.attributes())\n } catch (error) {\n channels.report(`${notice.label}: the domain's notice channels could not be read - in the app only`, error)\n }\n\n const email = turnedOn.email && notice.channels?.email !== false\n const teamsChannelWebhook = notice.broadcast && notice.channels?.teams !== false ? turnedOn.teamsChannelWebhook : undefined\n\n for (const receiver of notice.receivers) {\n const lng = languageOf(receiver.locale, notice.defaultLanguage)\n let words: NoticeWords\n try {\n words = notice.words(lng)\n } catch (error) {\n channels.report(`${notice.label}: the words for ${receiver.id} could not be made`, error)\n if (email && receiver.email) result.email.failed++\n continue\n }\n\n if (channels.push) {\n for (const mode of ['foreground', 'background'] as const) {\n try {\n await channels.push({\n receivers: [receiver.id],\n type: notice.type,\n subject: notice.level || 'info',\n title: words.subject,\n body: words.summary ?? words.body,\n url: notice.address,\n mode\n })\n } catch (error) {\n channels.report(`${notice.label}: the ${mode} notice to ${receiver.id} failed`, error)\n }\n }\n }\n\n if (email && receiver.email) {\n try {\n const sent = await channels.mail({\n receiver: receiver.email,\n subject: words.subject,\n content: mailForm({ lang: lng, words, link: notice.link, format: notice.format, base: baseOf(notice), brand: channels.brand })\n })\n if (sent) {\n result.email.sent++\n } else {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} did not go out`)\n }\n } catch (error) {\n result.email.failed++\n channels.report(`${notice.label}: the mail to ${receiver.email} failed`, error)\n }\n }\n }\n\n if (teamsChannelWebhook) {\n try {\n const answer = await channels.teams(teamsChannelWebhook, teamsMessage({ words: notice.words(languageOf(undefined, notice.defaultLanguage)), link: notice.link }))\n if (answer.ok) {\n result.teams.sent++\n } else {\n result.teams.failed++\n channels.report(`${notice.label}: Teams refused the channel post (${answer.status ?? '-'})`)\n }\n } catch (error) {\n result.teams.failed++\n channels.report(`${notice.label}: the Teams channel post failed`, error)\n }\n }\n\n return result\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function workerAddressOf(domain: any, url: string): string;
|
|
1
2
|
/**
|
|
2
3
|
* Tells the people of `domain` - by web push (`background`) or live to the screens they have open (`foreground`).
|
|
3
4
|
* The middleware gives each request one as `context.state.notify`; work that runs with no request (a scheduled
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.workerAddressOf = workerAddressOf;
|
|
3
4
|
exports.notifierOf = notifierOf;
|
|
4
5
|
exports.notificationMiddleware = notificationMiddleware;
|
|
5
6
|
const shell_1 = require("@things-factory/shell");
|
|
6
7
|
const env_1 = require("@things-factory/env");
|
|
7
8
|
const fcm_1 = require("../controllers/fcm");
|
|
8
9
|
const debug = require('debug')('things-factory:notification:notification-middleware');
|
|
10
|
+
/*
|
|
11
|
+
* The page a web push opens. A notice names its page relative to the domain, as the open screen navigates; the service
|
|
12
|
+
* worker that opens a web push knows no domain, so the page is handed to it from the origin - `/domain/<x>/<page>`,
|
|
13
|
+
* which needs no host and so holds for a notice sent with no request. An address with a scheme or from the origin is
|
|
14
|
+
* handed on as it is.
|
|
15
|
+
*/
|
|
16
|
+
function workerAddressOf(domain, url) {
|
|
17
|
+
return /^([a-z][a-z0-9+.-]*:|\/)/i.test(url) ? url : `/${(0, shell_1.getContextPath)(domain)}${url}`;
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* Tells the people of `domain` - by web push (`background`) or live to the screens they have open (`foreground`).
|
|
11
21
|
* The middleware gives each request one as `context.state.notify`; work that runs with no request (a scheduled
|
|
@@ -24,7 +34,7 @@ function notifierOf(domain) {
|
|
|
24
34
|
body,
|
|
25
35
|
data: {
|
|
26
36
|
type,
|
|
27
|
-
url,
|
|
37
|
+
url: url && workerAddressOf(domain, url),
|
|
28
38
|
timestamp: String(timestamp)
|
|
29
39
|
},
|
|
30
40
|
image,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-middleware.js","sourceRoot":"","sources":["../../server/middlewares/notification-middleware.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"notification-middleware.js","sourceRoot":"","sources":["../../server/middlewares/notification-middleware.ts"],"names":[],"mappings":";;AAaA,0CAEC;AAOD,gCAsDC;AAED,wDAOC;AArFD,iDAA8D;AAC9D,6CAA4C;AAE5C,4CAA2C;AAE3C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,qDAAqD,CAAC,CAAA;AAErF;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,MAAM,EAAE,GAAW;IACjD,OAAO,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAA,sBAAc,EAAC,MAAM,CAAC,GAAG,GAAG,EAAE,CAAA;AACzF,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,MAAM;IAC/B,OAAO,KAAK,WAAW,EACrB,SAAS,EACT,MAAM,EACN,KAAK,EACL,IAAI,EACJ,OAAO,GAAG,MAAM,EAChB,KAAK,EACL,IAAI,EACJ,KAAK,EACL,GAAG,EACH,OAAO,EACP,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EACtB,IAAI,GAAG,YAAY,EACpB;QACC,IAAI,CAAC;YACH,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC1B,2CAA2C;gBAE3C,MAAM,IAAA,YAAM,EAAC;oBACX,SAAS;oBACT,MAAM;oBACN,KAAK;oBACL,KAAK;oBACL,IAAI;oBACJ,IAAI,EAAE;wBACJ,IAAI;wBACJ,GAAG,EAAE,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,GAAG,CAAC;wBACxC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC;qBAC7B;oBACD,KAAK;oBACL,OAAO;iBACR,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,0FAA0F;gBAC1F,2BAA2B;gBAC3B,MAAM,cAAM,CAAC,OAAO,CAAC,cAAc,EAAE;oBACnC,YAAY,EAAE;wBACZ,MAAM;wBACN,SAAS;wBACT,OAAO;wBACP,IAAI;wBACJ,KAAK;wBACL,IAAI;wBACJ,KAAK;wBACL,GAAG;wBACH,SAAS;qBACV;iBACF,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnB,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,sBAAsB,CAAC,OAAY,EAAE,IAAS;IAClE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC1B,sGAAsG;QACtG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM,IAAI,EAAE,CAAA;AACd,CAAC","sourcesContent":["import { getContextPath, pubsub } from '@things-factory/shell'\nimport { logger } from '@things-factory/env'\n\nimport { notify } from '../controllers/fcm'\n\nconst debug = require('debug')('things-factory:notification:notification-middleware')\n\n/*\n * The page a web push opens. A notice names its page relative to the domain, as the open screen navigates; the service\n * worker that opens a web push knows no domain, so the page is handed to it from the origin - `/domain/<x>/<page>`,\n * which needs no host and so holds for a notice sent with no request. An address with a scheme or from the origin is\n * handed on as it is.\n */\nexport function workerAddressOf(domain, url: string): string {\n return /^([a-z][a-z0-9+.-]*:|\\/)/i.test(url) ? url : `/${getContextPath(domain)}${url}`\n}\n\n/**\n * Tells the people of `domain` - by web push (`background`) or live to the screens they have open (`foreground`).\n * The middleware gives each request one as `context.state.notify`; work that runs with no request (a scheduled\n * announcement) takes one for its domain.\n */\nexport function notifierOf(domain) {\n return async function ({\n receivers,\n tokens,\n topic,\n type,\n subject = 'info',\n title,\n body,\n image,\n url,\n actions,\n timestamp = Date.now(),\n mode = 'background'\n }) {\n try {\n if (mode === 'background') {\n /* for send webpush notification message */\n\n await notify({\n receivers,\n tokens,\n topic,\n title,\n body,\n data: {\n type,\n url: url && workerAddressOf(domain, url),\n timestamp: String(timestamp)\n },\n image,\n actions\n })\n } else {\n /* for send publish notification to clients - to the receivers only, when there are any */\n // TODO CONFIRM data format\n await pubsub.publish('notification', {\n notification: {\n domain,\n receivers,\n subject,\n type,\n title,\n body,\n image,\n url,\n timestamp\n }\n })\n }\n } catch (err) {\n logger.error(err)\n }\n }\n}\n\nexport async function notificationMiddleware(context: any, next: any) {\n if (!context.state.notify) {\n /* the domain as it stands when the notice is sent - a later middleware may be the one that sets it */\n context.state.notify = payload => notifierOf(context.state.domain)(payload)\n }\n\n await next()\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './notification-rule/notification-rule';
|
|
2
2
|
export * from './notification/notification';
|
|
3
|
-
export
|
|
3
|
+
export * from './notification-token/notification-token';
|
|
4
|
+
export declare const entities: (typeof import("./notification-rule/notification-rule").NotificationRule | typeof import("./notification/notification").Notification | typeof import("./notification-token/notification-token").NotificationToken)[];
|
|
4
5
|
export declare const subscribers: typeof import("./notification-rule/event-subscriber").NotificationRuleHistoryEntitySubscriber[];
|
|
5
6
|
export declare const schema: {
|
|
6
7
|
typeDefs: {
|
|
@@ -5,14 +5,17 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
/* EXPORT ENTITY TYPES */
|
|
6
6
|
tslib_1.__exportStar(require("./notification-rule/notification-rule"), exports);
|
|
7
7
|
tslib_1.__exportStar(require("./notification/notification"), exports);
|
|
8
|
+
tslib_1.__exportStar(require("./notification-token/notification-token"), exports);
|
|
8
9
|
/* IMPORT ENTITIES AND RESOLVERS */
|
|
9
10
|
const notification_rule_1 = require("./notification-rule");
|
|
10
11
|
const notification_1 = require("./notification");
|
|
11
12
|
const notice_channel_1 = require("./notice-channel");
|
|
13
|
+
const notification_token_1 = require("./notification-token");
|
|
12
14
|
exports.entities = [
|
|
13
15
|
/* ENTITIES */
|
|
14
16
|
...notification_rule_1.entities,
|
|
15
|
-
...notification_1.entities
|
|
17
|
+
...notification_1.entities,
|
|
18
|
+
...notification_token_1.entities
|
|
16
19
|
];
|
|
17
20
|
exports.subscribers = [
|
|
18
21
|
/* SUBSCRIBERS */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,gFAAqD;AACrD,sEAA2C;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,gFAAqD;AACrD,sEAA2C;AAC3C,kFAAuD;AAEvD,mCAAmC;AACnC,2DAI4B;AAC5B,iDAKuB;AACvB,qDAAsE;AACtE,6DAA4E;AAE/D,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,4BAAwB;IAC3B,GAAG,uBAAoB;IACvB,GAAG,6BAAyB;CAC7B,CAAA;AAEY,QAAA,WAAW,GAAG;IACzB,iBAAiB;IACjB,GAAG,+BAA2B;CAC/B,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,GAAG,uBAAoB;KACxB;IACD,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,6BAAyB;QAC5B,GAAG,wBAAqB;QACxB,GAAG,wBAAqB;QACxB,GAAG,0BAAsB;KAC1B;IACD,UAAU,EAAE;QACV,GAAG,yBAAsB;KAC1B;CACF,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './notification-rule/notification-rule'\nexport * from './notification/notification'\nexport * from './notification-token/notification-token'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as NotificationRuleEntities,\n resolvers as NotificationRuleResolvers,\n subscribers as NotificationRuleSubscribers\n} from './notification-rule'\nimport {\n entities as NotificationEntities,\n typeDefs as NotificationTypeDefs,\n resolvers as NotificationResolvers,\n directives as NotificationDirectives\n} from './notification'\nimport { resolvers as NoticeChannelResolvers } from './notice-channel'\nimport { entities as NotificationTokenEntities } from './notification-token'\n\nexport const entities = [\n /* ENTITIES */\n ...NotificationRuleEntities,\n ...NotificationEntities,\n ...NotificationTokenEntities\n]\n\nexport const subscribers = [\n /* SUBSCRIBERS */\n ...NotificationRuleSubscribers\n]\n\nexport const schema = {\n typeDefs: {\n ...NotificationTypeDefs\n },\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...NotificationRuleResolvers,\n ...NotificationResolvers,\n ...NotificationResolvers,\n ...NoticeChannelResolvers\n ],\n directives: {\n ...NotificationDirectives\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.entities = void 0;
|
|
4
|
+
const notification_token_1 = require("./notification-token");
|
|
5
|
+
exports.entities = [notification_token_1.NotificationToken];
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/notification-token/index.ts"],"names":[],"mappings":";;;AAAA,6DAAwD;AAE3C,QAAA,QAAQ,GAAG,CAAC,sCAAiB,CAAC,CAAA","sourcesContent":["import { NotificationToken } from './notification-token'\n\nexport const entities = [NotificationToken]\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { User } from '@things-factory/auth-base';
|
|
2
|
+
/**
|
|
3
|
+
* One browser or installed app of a person that web push reaches - the FCM registration token it was given.
|
|
4
|
+
*
|
|
5
|
+
* A token belongs to the person, not to a domain: the person's phone hears from every domain they belong to, and a
|
|
6
|
+
* notice carries its own full address. A token names one browser, so it is held by one person at a time - when
|
|
7
|
+
* someone else signs in on that browser and turns notices on, the token moves to them.
|
|
8
|
+
*
|
|
9
|
+
* It is a device handle, not a record: a token turned off or refused by FCM is deleted, not kept.
|
|
10
|
+
*/
|
|
11
|
+
export declare class NotificationToken {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
user?: User;
|
|
14
|
+
userId?: string;
|
|
15
|
+
token: string;
|
|
16
|
+
createdAt?: Date;
|
|
17
|
+
updatedAt?: Date;
|
|
18
|
+
creator?: User;
|
|
19
|
+
creatorId?: string;
|
|
20
|
+
updater?: User;
|
|
21
|
+
updaterId?: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationToken = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
7
|
+
/**
|
|
8
|
+
* One browser or installed app of a person that web push reaches - the FCM registration token it was given.
|
|
9
|
+
*
|
|
10
|
+
* A token belongs to the person, not to a domain: the person's phone hears from every domain they belong to, and a
|
|
11
|
+
* notice carries its own full address. A token names one browser, so it is held by one person at a time - when
|
|
12
|
+
* someone else signs in on that browser and turns notices on, the token moves to them.
|
|
13
|
+
*
|
|
14
|
+
* It is a device handle, not a record: a token turned off or refused by FCM is deleted, not kept.
|
|
15
|
+
*/
|
|
16
|
+
let NotificationToken = class NotificationToken {
|
|
17
|
+
};
|
|
18
|
+
exports.NotificationToken = NotificationToken;
|
|
19
|
+
tslib_1.__decorate([
|
|
20
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
21
|
+
tslib_1.__metadata("design:type", String)
|
|
22
|
+
], NotificationToken.prototype, "id", void 0);
|
|
23
|
+
tslib_1.__decorate([
|
|
24
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { onDelete: 'CASCADE' }),
|
|
25
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
26
|
+
], NotificationToken.prototype, "user", void 0);
|
|
27
|
+
tslib_1.__decorate([
|
|
28
|
+
(0, typeorm_1.RelationId)((notificationToken) => notificationToken.user),
|
|
29
|
+
tslib_1.__metadata("design:type", String)
|
|
30
|
+
], NotificationToken.prototype, "userId", void 0);
|
|
31
|
+
tslib_1.__decorate([
|
|
32
|
+
(0, typeorm_1.Column)({ length: 512 }),
|
|
33
|
+
tslib_1.__metadata("design:type", String)
|
|
34
|
+
], NotificationToken.prototype, "token", void 0);
|
|
35
|
+
tslib_1.__decorate([
|
|
36
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
37
|
+
tslib_1.__metadata("design:type", Date)
|
|
38
|
+
], NotificationToken.prototype, "createdAt", void 0);
|
|
39
|
+
tslib_1.__decorate([
|
|
40
|
+
(0, typeorm_1.UpdateDateColumn)(),
|
|
41
|
+
tslib_1.__metadata("design:type", Date)
|
|
42
|
+
], NotificationToken.prototype, "updatedAt", void 0);
|
|
43
|
+
tslib_1.__decorate([
|
|
44
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
45
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
46
|
+
], NotificationToken.prototype, "creator", void 0);
|
|
47
|
+
tslib_1.__decorate([
|
|
48
|
+
(0, typeorm_1.RelationId)((notificationToken) => notificationToken.creator),
|
|
49
|
+
tslib_1.__metadata("design:type", String)
|
|
50
|
+
], NotificationToken.prototype, "creatorId", void 0);
|
|
51
|
+
tslib_1.__decorate([
|
|
52
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
53
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
54
|
+
], NotificationToken.prototype, "updater", void 0);
|
|
55
|
+
tslib_1.__decorate([
|
|
56
|
+
(0, typeorm_1.RelationId)((notificationToken) => notificationToken.updater),
|
|
57
|
+
tslib_1.__metadata("design:type", String)
|
|
58
|
+
], NotificationToken.prototype, "updaterId", void 0);
|
|
59
|
+
exports.NotificationToken = NotificationToken = tslib_1.__decorate([
|
|
60
|
+
(0, typeorm_1.Entity)(),
|
|
61
|
+
(0, typeorm_1.Index)('ix_notification_token_0', (notificationToken) => [notificationToken.token], { unique: true }),
|
|
62
|
+
(0, typeorm_1.Index)('ix_notification_token_1', (notificationToken) => [notificationToken.user], { unique: false })
|
|
63
|
+
], NotificationToken);
|
|
64
|
+
//# sourceMappingURL=notification-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-token.js","sourceRoot":"","sources":["../../../server/service/notification-token/notification-token.ts"],"names":[],"mappings":";;;;AAAA,qCAAkI;AAElI,yDAAgD;AAEhD;;;;;;;;GAQG;AAII,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;CA+B7B,CAAA;AA/BY,8CAAiB;AAEnB;IADR,IAAA,gCAAsB,EAAC,MAAM,CAAC;;6CACZ;AAGnB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;sCAC1C,gBAAI;+CAAA;AAGX;IADC,IAAA,oBAAU,EAAC,CAAC,iBAAoC,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC;;iDAC9D;AAIf;IADC,IAAA,gBAAM,EAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;;gDACX;AAGb;IADC,IAAA,0BAAgB,GAAE;sCACP,IAAI;oDAAA;AAGhB;IADC,IAAA,0BAAgB,GAAE;sCACP,IAAI;oDAAA;AAGhB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAClC,gBAAI;kDAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,iBAAoC,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC;;oDAC9D;AAGlB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAClC,gBAAI;kDAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,iBAAoC,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC;;oDAC9D;4BA9BP,iBAAiB;IAH7B,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,yBAAyB,EAAE,CAAC,iBAAoC,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvH,IAAA,eAAK,EAAC,yBAAyB,EAAE,CAAC,iBAAoC,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;GAC3G,iBAAiB,CA+B7B","sourcesContent":["import { CreateDateColumn, UpdateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\n\n/**\n * One browser or installed app of a person that web push reaches - the FCM registration token it was given.\n *\n * A token belongs to the person, not to a domain: the person's phone hears from every domain they belong to, and a\n * notice carries its own full address. A token names one browser, so it is held by one person at a time - when\n * someone else signs in on that browser and turns notices on, the token moves to them.\n *\n * It is a device handle, not a record: a token turned off or refused by FCM is deleted, not kept.\n */\n@Entity()\n@Index('ix_notification_token_0', (notificationToken: NotificationToken) => [notificationToken.token], { unique: true })\n@Index('ix_notification_token_1', (notificationToken: NotificationToken) => [notificationToken.user], { unique: false })\nexport class NotificationToken {\n @PrimaryGeneratedColumn('uuid')\n readonly id: string\n\n @ManyToOne(type => User, { onDelete: 'CASCADE' })\n user?: User\n\n @RelationId((notificationToken: NotificationToken) => notificationToken.user)\n userId?: string\n\n /* FCM tokens run to about 160 characters today; the room is for what FCM may hand out later. */\n @Column({ length: 512 })\n token: string\n\n @CreateDateColumn()\n createdAt?: Date\n\n @UpdateDateColumn()\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n creator?: User\n\n @RelationId((notificationToken: NotificationToken) => notificationToken.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n updater?: User\n\n @RelationId((notificationToken: NotificationToken) => notificationToken.updater)\n updaterId?: string\n}\n"]}
|