backend-manager 4.1.2 → 4.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "4.1.2",
3
+ "version": "4.1.3",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -205,7 +205,7 @@ Module.prototype.getAllUsers = function () {
205
205
  Module.prototype.getAllNotifications = function () {
206
206
  const self = this;
207
207
  return new Promise(async function(resolve, reject) {
208
- await self.libraries.admin.firestore().collection('notifications/subscriptions/all')
208
+ await self.libraries.admin.firestore().collection('notifications')
209
209
  .get()
210
210
  .then(function(querySnapshot) {
211
211
  return resolve(querySnapshot.size)
@@ -1,20 +1,26 @@
1
- const path_processing = 'notifications/processing/all/{notificationId}';
2
- const path_subscriptions = 'notifications/subscriptions/all';
3
- const badTokenReasons = ['messaging/invalid-registration-token', 'messaging/registration-token-not-registered']
4
-
1
+ // Constants
2
+ const PATH_NOTIFICATIONS = 'notifications';
3
+ const BAD_TOKEN_REASONS = [
4
+ 'messaging/invalid-registration-token',
5
+ 'messaging/registration-token-not-registered',
6
+ ];
7
+
8
+ // Module
5
9
  function Module() {
6
10
 
7
11
  }
8
12
 
13
+ // Main
9
14
  Module.prototype.main = function () {
10
15
  const self = this;
16
+
17
+ // Shortcuts
11
18
  const Manager = self.Manager;
12
19
  const Api = self.Api;
13
20
  const assistant = self.assistant;
14
21
  const payload = self.payload;
15
22
 
16
23
  return new Promise(async function(resolve, reject) {
17
-
18
24
  // Set up response obj
19
25
  payload.response.data = {
20
26
  subscribers: 0,
@@ -24,33 +30,39 @@ Module.prototype.main = function () {
24
30
  }
25
31
 
26
32
  // Fix notification payload
27
- self._notificationPayload = {
28
- notification: self.payload.data.payload.notification || {},
29
- };
30
- self._notificationPayload.notification.title = self.payload.data.payload.title || self.payload.data.payload.notification.title || 'Notification';
31
- self._notificationPayload.notification.click_action = self.payload.data.payload.click_action || self.payload.data.payload.notification.click_action || 'https://itwcreativeworks.com';
32
- self._notificationPayload.notification.body = self.payload.data.payload.body || self.payload.data.payload.notification.body || 'Check this out';
33
- self._notificationPayload.notification.icon = self.payload.data.payload.icon || self.payload.data.payload.notification.icon || self.Manager.config.brand.brandmark || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png';
33
+ // Yes, it needs to be NESTED!!! DO NOT REMOVE THE NEST!
34
+ const note = {
35
+ notification: {
36
+ title: payload.data.payload.title || 'Notification',
37
+ body: payload.data.payload.body || 'Check this out',
38
+ icon: payload.data.payload.icon || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png',
39
+ click_action: payload.data.payload.clickAction || 'https://itwcreativeworks.com',
40
+ }
41
+ }
34
42
 
43
+ // Set notification payload
35
44
  try {
36
- self._notificationPayload.notification.click_action = new URL(self._notificationPayload.notification.click_action);
37
- self._notificationPayload.notification.click_action.searchParams.set('cb', new Date().getTime())
38
- self._notificationPayload.notification.click_action = self._notificationPayload.notification.click_action.toString()
45
+ const url = new URL(note.notification.click_action);
46
+ url.searchParams.set('cb', new Date().getTime());
47
+ note.notification.click_action = url.toString();
39
48
  } catch (e) {
40
- assistant.errorify(`Failed to add cb to URL: ${e}`, {code: 500, log: true});
49
+ reject(assistant.errorify(`Failed to add cb to URL: ${e}`, {code: 400, log: true}));
41
50
  }
42
51
 
43
- assistant.log('Resolved notification payload', self._notificationPayload)
52
+ // Log
53
+ assistant.log('Resolved notification payload', note)
44
54
 
55
+ // Check if user is admin
45
56
  if (!payload.user.roles.admin) {
46
57
  return reject(assistant.errorify(`Admin required.`, {code: 401}));
47
58
  }
48
59
 
49
- if (!payload.data.payload.title || !payload.data.payload.body) {
60
+ // Check if title and body are set
61
+ if (!note.notification.title || !note.notification.body) {
50
62
  return reject(assistant.errorify(`Parameters <title> and <body> required`, {code: 400, sentry: true}));
51
63
  }
52
64
 
53
- await self.getTokens({tags: false})
65
+ await self.processTokens(note, {tags: false})
54
66
  .then(r => {
55
67
  return resolve({data: payload.response.data})
56
68
  })
@@ -62,136 +74,158 @@ Module.prototype.main = function () {
62
74
  };
63
75
 
64
76
  // HELPERS //
65
- Module.prototype.getTokens = function (options) {
77
+ Module.prototype.processTokens = async function (note, options) {
66
78
  const self = this;
79
+
80
+ // Shortcuts
81
+ const Manager = self.Manager;
82
+ const Api = self.Api;
83
+ const assistant = self.assistant;
84
+ const payload = self.payload;
85
+
86
+ // Set options
67
87
  options = options || {};
68
88
  options.tags = options.tags || false;
69
89
 
70
- return new Promise(async function(resolve, reject) {
71
- let subs = self.libraries.admin.firestore().collection(path_subscriptions);
72
- let batchPromises = [];
90
+ // Define collection path
91
+ const queryConditions = options.tags
92
+ ? [{ field: 'tags', operator: 'array-contains-any', value: options.tags }]
93
+ : [];
73
94
 
74
- if (options.tags) {
75
- subs.where('tags', 'array-contains-any', options.tags)
76
- }
77
- await subs
78
- .get()
79
- .then(function(querySnapshot) {
80
- self.assistant.log(`Queried ${querySnapshot.size} tokens.`);
81
- self.payload.response.data.subscribers = querySnapshot.size;
82
- // self.result.subscriptionsStart = querySnapshot.size;
83
- let batchCurrentSize = 0;
84
- let batchSizeMax = 1000;
85
-
86
- let batchCurrent = [];
87
- let batchLoops = 1;
88
-
89
- querySnapshot.forEach(function(doc) {
90
- // log(self, 'loading... ', batchLoops+'/'+querySnapshot.size);
91
- if ((batchCurrentSize < batchSizeMax - 1) && (batchLoops < querySnapshot.size)) {
92
- batchCurrent.push(doc.data().token);
93
- batchCurrentSize++;
94
- } else {
95
- let batchId = batchPromises.length + 1;
96
- batchCurrent.push(doc.data().token);
97
- batchCurrentSize++;
98
- console.log(`Got batch ID: ${batchId} with ${batchCurrentSize} tokens.`);
99
- batchPromises.push(self.sendBatch(batchCurrent, batchId));
100
- batchCurrent = [];
101
- batchCurrentSize = 0;
102
- self.payload.response.data.batches++;
103
- }
104
- batchLoops++;
105
- });
106
- })
107
- .catch(function(e) {
108
- self.assistant.error('Error querying tokens: ', e)
109
- reject(error);
110
- });
111
-
112
- await Promise.all(batchPromises)
113
- .then(function(values) {
114
- self.assistant.log('Finished all batches.');
115
- })
116
- .catch(function(e) {
117
- self.assistant.error('Error sending batches: ', e)
118
- });
119
- resolve();
95
+ // Batch processing logic
96
+ await Manager.Utilities().iterateCollection(
97
+ async (batch, index) => {
98
+ let batchTokens = [];
99
+
100
+ // Collect tokens from the current batch
101
+ for (const doc of batch.docs) {
102
+ const data = doc.data();
103
+ batchTokens.push(data.token);
104
+ }
120
105
 
106
+ // Send the batch
107
+ try {
108
+ assistant.log(`Sending batch ${index} with ${batchTokens.length} tokens.`);
109
+ await self.sendBatch(batchTokens, note, index);
110
+ } catch (e) {
111
+ assistant.error(`Error sending batch ${index}`, e);
112
+ }
113
+ },
114
+ {
115
+ collection: PATH_NOTIFICATIONS,
116
+ where: queryConditions,
117
+ batchSize: 1000,
118
+ log: true,
119
+ }
120
+ )
121
+ .then(() => {
122
+ assistant.log('All batches processed successfully.');
123
+ })
124
+ .catch(e => {
125
+ assistant.errorify(`Error during token processing: ${e}`, { code: 500, log: true });
121
126
  });
122
- }
127
+ };
123
128
 
124
- Module.prototype.sendBatch = function (batch, id) {
129
+ Module.prototype.sendBatch = async function (batch, note, id) {
125
130
  const self = this;
126
- return new Promise(async function(resolve, reject) {
127
- // self.assistant.log(`Sending batch ID: ${id}`, batch);
128
- self.assistant.log(`Sending batch ID: ${id}`);
129
-
130
- // self.assistant.log('payload', payload);
131
-
132
- await self.libraries.admin.messaging().sendToDevice(batch, self._notificationPayload)
133
- .then(async function (response) {
134
- // self.result.batches.list.push('#' + id + ' | ' + '✅ ' + response.successCount + ' | ' + '❌ ' + response.failureCount);
135
- self.assistant.log('Sent batch #' + id);
136
- // self.result.successes += response.successCount;
137
- // self.result.failures += response.failureCount;
138
- // console.log('RESP', response);
139
- if (response.failureCount > 0) {
140
- await self.cleanTokens(batch, response.results, id);
141
- }
142
- self.payload.response.data.sent += (batch.length - response.failureCount);
143
- resolve();
144
- })
145
- .catch(function (e) {
146
- self.assistant.error('Error sending batch #' + id, e);
147
- // self.result.status = 'fail';
148
- reject(e);
149
- })
150
- });
151
- }
152
131
 
153
- Module.prototype.cleanTokens = function (batch, results, id) {
132
+ // Shortcuts
133
+ const Manager = self.Manager;
134
+ const Api = self.Api;
135
+ const assistant = self.assistant;
136
+ const payload = self.payload;
137
+
138
+ // Libraries
139
+ const { admin } = self.libraries;
140
+
141
+ try {
142
+ // Log
143
+ assistant.log(`Sending batch ID: ${id}`);
144
+
145
+ // Send the batch
146
+ const response = await admin.messaging().sendToDevice(batch, note);
147
+
148
+ // Log
149
+ assistant.log(`Sent batch ID: ${id}, Success: ${response.successCount}, Failures: ${response.failureCount}`);
150
+
151
+ // Clean bad tokens
152
+ if (response.failureCount > 0) {
153
+ await self.cleanTokens(batch, response.results, id);
154
+ }
155
+
156
+ // Update response
157
+ payload.response.data.sent += (batch.length - response.failureCount);
158
+ } catch (e) {
159
+ throw assistant.errorify(`Error sending batch ${id}: ${e}`, { code: 500, log: true });
160
+ }
161
+ };
162
+
163
+ Module.prototype.cleanTokens = async function (batch, results, id) {
154
164
  const self = this;
155
165
 
156
- return new Promise(async function(resolve, reject) {
157
- let cleanPromises = [];
158
- // self.assistant.log(`Cleaning tokens of batch ID: ${id}`, results);
159
- self.assistant.log(`Cleaning tokens of batch ID: ${id}`);
166
+ // Shortcuts
167
+ const Manager = self.Manager;
168
+ const Api = self.Api;
169
+ const assistant = self.assistant;
170
+ const payload = self.payload;
160
171
 
161
- results.forEach(function (item, index) {
162
- if (!item.error) {
163
- return false;
164
- }
165
- let curCode = item.error.code;
166
- let token = batch[index];
167
- self.assistant.log(`Found bad token: ${index} = ${curCode}`);
168
- if (badTokenReasons.includes(curCode)) {
169
- cleanPromises.push(self.deleteToken(token, curCode));
172
+ // Log
173
+ assistant.log(`Cleaning tokens of batch ID: ${id}`);
174
+
175
+ // Filter out bad tokens
176
+ const cleanPromises = results
177
+ .map((item, index) => {
178
+ // Check if the token is bad
179
+ if (item.error && BAD_TOKEN_REASONS.includes(item.error.code)) {
180
+ const token = batch[index];
181
+
182
+ // Log
183
+ assistant.log(`Found bad token: ${token} (Reason: ${item.error.code})`);
184
+
185
+ // Delete the token
186
+ return self.deleteToken(token, item.error.code);
170
187
  }
188
+
189
+ // Return null for valid tokens
190
+ return null;
171
191
  })
172
- await Promise.all(cleanPromises)
173
- .catch(function(e) {
174
- self.assistant.log('error', "Error cleaning failed tokens: ", e);
175
- });
176
- resolve();
177
- });
178
- }
192
+ // Filter out nulls for valid tokens
193
+ .filter(Boolean);
194
+
195
+ // Clean bad tokens
196
+ try {
197
+ await Promise.all(cleanPromises);
198
+ assistant.log(`Completed cleaning tokens for batch ID: ${id}`);
199
+ } catch (e) {
200
+ assistant.error(`Error cleaning tokens for batch ID: ${id}`, e);
201
+ }
202
+ };
179
203
 
180
- Module.prototype.deleteToken = function (token, errorCode) {
204
+ Module.prototype.deleteToken = async function (token, errorCode) {
181
205
  const self = this;
182
- return new Promise(function(resolve, reject) {
183
- self.libraries.admin.firestore().doc(`${path_subscriptions}/${token}`)
184
- .delete()
185
- .then(function() {
186
- self.assistant.log(`Deleting bad token: ${token} for reason ${errorCode}`);
187
- self.payload.response.data.deleted++;
188
- resolve();
189
- })
190
- .catch(function(error) {
191
- self.assistant.log('error', `Error deleting bad token: ${token} for reason ${errorCode} because of error ${error}`);
192
- resolve();
193
- })
194
- });
195
- }
206
+
207
+ // Shortcuts
208
+ const Manager = self.Manager;
209
+ const Api = self.Api;
210
+ const assistant = self.assistant;
211
+ const payload = self.payload;
212
+
213
+ // Libraries
214
+ const { admin } = self.libraries;
215
+
216
+ // Delete the token
217
+ try {
218
+ // Delete the token
219
+ await admin.firestore().doc(`${PATH_NOTIFICATIONS}/${token}`).delete();
220
+
221
+ // Log
222
+ assistant.log(`Deleted bad token: ${token} (Reason: ${errorCode})`);
223
+
224
+ // Update response
225
+ payload.response.data.deleted++;
226
+ } catch (error) {
227
+ assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode}). Error: ${error}`);
228
+ }
229
+ };
196
230
 
197
231
  module.exports = Module;
@@ -9,6 +9,8 @@ function Module() {
9
9
 
10
10
  Module.prototype.init = function (Manager, data) {
11
11
  const self = this;
12
+
13
+ // Set properties
12
14
  self.Manager = Manager;
13
15
  self.libraries = Manager.libraries;
14
16
  self.assistant = Manager.Assistant({req: data.req, res: data.res})
@@ -30,15 +32,14 @@ Module.prototype.init = function (Manager, data) {
30
32
  self.assistant.request.data.payload = self.assistant.request.data.payload || {};
31
33
  self.assistant.request.data.options = self.assistant.request.data.options || {};
32
34
 
33
- if (Manager.options.log) {
34
- self.assistant.log(`Executing (log): ${resolved.command}`, self.assistant.request, JSON.stringify(self.assistant.request))
35
- }
36
-
35
+ // Return
37
36
  return self;
38
37
  }
39
38
 
40
39
  Module.prototype.main = function() {
41
40
  const self = this;
41
+
42
+ // Shortcuts
42
43
  const Manager = self.Manager;
43
44
  const libraries = self.libraries;
44
45
  const assistant = self.assistant;
@@ -47,19 +48,34 @@ Module.prototype.main = function() {
47
48
 
48
49
  return new Promise(async function(resolve, reject) {
49
50
  return libraries.cors(req, res, async () => {
51
+ // Set properties
50
52
  self.payload.data = assistant.request.data;
51
53
  self.payload.user = await assistant.authenticate();
52
54
 
55
+ // Set properties
56
+ const headers = assistant.request.headers;
57
+ const method = assistant.request.method;
58
+ const url = assistant.request.url;
59
+ const geolocation = assistant.request.geolocation;
60
+ const client = assistant.request.client;
61
+
62
+ // Strip URL
63
+ const strippedUrl = stripUrl(url);
64
+
53
65
  // Quit if OPTIONS request
54
- if (self.assistant.request.method === 'OPTIONS') {
66
+ if (method === 'OPTIONS') {
55
67
  return resolve();
56
68
  }
57
69
 
58
70
  // Resolve command
59
71
  const resolved = self.resolveCommand(self.payload.data.command);
60
72
 
61
- self.assistant.log(`Executing: ${resolved.command}`, self.payload, JSON.stringify(self.payload))
62
- self.assistant.log(`Resolved URL: ${Manager.project.functionsUrl}?command=${encodeURIComponent(resolved.command)}&payload=${encodeURIComponent(JSON.stringify(self.assistant.request.data.payload))}`)
73
+ // Log
74
+ // assistant.log(`Executing: ${resolved.command}`, self.payload, JSON.stringify(self.payload))
75
+ // assistant.log(`Resolved URL: ${Manager.project.functionsUrl}?command=${encodeURIComponent(resolved.command)}&payload=${encodeURIComponent(JSON.stringify(self.assistant.request.data.payload))}`)
76
+ assistant.log(`bm_api(${resolved.command}): Request (${geolocation.ip} @ ${geolocation.country}, ${geolocation.region}, ${geolocation.city}) [${method} > ${strippedUrl}]`, JSON.stringify(assistant.request.data));
77
+ assistant.log(`bm_api(${resolved.command}): Headers`, JSON.stringify(headers));
78
+
63
79
 
64
80
  // Set up options
65
81
  self.payload.data.options = self.payload.data.options || {};
@@ -70,7 +86,7 @@ Module.prototype.main = function() {
70
86
  let delay = Math.floor(self.payload.data.options.delay / 1000);
71
87
 
72
88
  await powertools.poll(() => {
73
- self.assistant.log(`Delaying for ${delay--} seconds...`);
89
+ assistant.log(`Delaying for ${delay--} seconds...`);
74
90
  }, {interval: 1000, timeout: self.payload.data.options.delay})
75
91
  .catch(e => e);
76
92
  }
@@ -116,7 +132,7 @@ Module.prototype.main = function() {
116
132
 
117
133
  // Send response
118
134
  if (self.payload.response.status >= 200 && self.payload.response.status < 399) {
119
- self.assistant.log(`Finished ${resolved.command} (status=${self.payload.response.status})`, self.payload, JSON.stringify(self.payload))
135
+ assistant.log(`Finished ${resolved.command} (status=${self.payload.response.status})`, self.payload, JSON.stringify(self.payload))
120
136
 
121
137
  if (self.payload.response.redirect) {
122
138
  res.redirect(self.payload.response.redirect);
@@ -126,7 +142,7 @@ Module.prototype.main = function() {
126
142
  return resolve();
127
143
  }
128
144
  } else {
129
- self.assistant.error(`Error executing ${resolved.command} @ ${resolved.path} (status=${self.payload.response.status}):`, self.payload.response.error)
145
+ assistant.error(`Error executing ${resolved.command} @ ${resolved.path} (status=${self.payload.response.status}):`, self.payload.response.error)
130
146
  res.send(`${self.payload.response.error}`)
131
147
  return reject(self.payload.response.error);
132
148
  }
@@ -188,8 +204,17 @@ Module.prototype.import = function (command, payload, user, response) {
188
204
 
189
205
  Module.prototype.resolveCommand = function (command) {
190
206
  const self = this;
207
+
208
+ // Shortcuts
209
+ const assistant = self.assistant;
210
+
211
+ // Set original command
191
212
  const originalCommand = command;
192
213
 
214
+ // Set properties
215
+ const method = assistant.request.method;
216
+
217
+ // Set command
193
218
  command = command || '';
194
219
 
195
220
  // Start
@@ -251,9 +276,9 @@ Module.prototype.resolveCommand = function (command) {
251
276
  // Check local path
252
277
  const resolvedPath = self.resolveApiPath(command);
253
278
 
254
- // if (!command || command === 'error:error') {
255
- if (!resolvedPath) {
256
- self.assistant.log(`This command does not exist: ${originalCommand} => ${command} @ ${resolvedPath}`)
279
+ // Log if command does not exist
280
+ if (method !== 'OPTIONS' && !resolvedPath) {
281
+ assistant.error(`This command does not exist: ${originalCommand} => ${command} @ ${resolvedPath}`)
257
282
  }
258
283
 
259
284
  return {
@@ -354,4 +379,10 @@ Module.prototype.resolveApiPath = function (command) {
354
379
  }
355
380
  };
356
381
 
382
+ function stripUrl(url) {
383
+ const newUrl = new URL(url);
384
+
385
+ return `${newUrl.hostname}${newUrl.pathname}`.replace(/\/$/, '');
386
+ }
387
+
357
388
  module.exports = Module;
@@ -179,7 +179,7 @@ let Module = {
179
179
  getAllSubscriptions: function () {
180
180
  let self = this;
181
181
  return new Promise(async function(resolve, reject) {
182
- await self.libraries.admin.firestore().collection('notifications/subscriptions/all')
182
+ await self.libraries.admin.firestore().collection('notifications')
183
183
  .get()
184
184
  .then(function(querySnapshot) {
185
185
  return resolve(querySnapshot.size)
@@ -49,12 +49,12 @@ Module.prototype.main = function () {
49
49
  if (eventType === 'delete') {
50
50
  await libraries.admin.firestore().doc(`meta/stats`)
51
51
  .update({
52
- 'subscriptions.total': libraries.admin.firestore.FieldValue.increment(-1),
52
+ 'notifications.total': libraries.admin.firestore.FieldValue.increment(-1),
53
53
  })
54
54
  .then(r => {
55
55
  analytics = self.Manager.Analytics({
56
56
  assistant: assistant,
57
- uuid: _.get(dataBefore, 'link.user.data.uid', undefined),
57
+ uuid: dataBefore?.owner?.uid,
58
58
  })
59
59
  .event({
60
60
  name: 'notification-unsubscribe',
@@ -78,12 +78,12 @@ Module.prototype.main = function () {
78
78
  } else if (eventType === 'create') {
79
79
  await libraries.admin.firestore().doc(`meta/stats`)
80
80
  .update({
81
- 'subscriptions.total': libraries.admin.firestore.FieldValue.increment(1),
81
+ 'notifications.total': libraries.admin.firestore.FieldValue.increment(1),
82
82
  })
83
83
  .then(r => {
84
84
  analytics = self.Manager.Analytics({
85
85
  assistant: assistant,
86
- uuid: _.get(dataAfter, 'link.user.data.uid', undefined),
86
+ uuid: dataAfter?.owner?.uid,
87
87
  })
88
88
  .event({
89
89
  name: 'notification-subscribe',
@@ -62,7 +62,7 @@ Settings.prototype.resolve = function (assistant, schema, settings, options) {
62
62
  // console.log('---self.settings', self.settings);
63
63
 
64
64
  // Iterate each key and check for some things
65
- processSchema(schema, (path, schemaNode) => {
65
+ iterateSchema(schema, (path, schemaNode) => {
66
66
  const originalValue = _.get(settings, path);
67
67
  const resolvedValue = _.get(self.settings, path);
68
68
  let replaceValue = undefined;
@@ -154,7 +154,7 @@ Settings.prototype.constant = function (name, options) {
154
154
  }
155
155
  };
156
156
 
157
- function processSchema(schema, fn, path) {
157
+ function iterateSchema(schema, fn, path) {
158
158
  path = path || '';
159
159
 
160
160
  // Base case: Check if the current level has 'types' and 'default', indicating metadata
@@ -167,7 +167,7 @@ function processSchema(schema, fn, path) {
167
167
  // Recursive case: Iterate through nested keys if we're not at a metadata node
168
168
  Object.keys(schema).forEach(key => {
169
169
  const nextPath = path ? `${path}.${key}` : key;
170
- processSchema(schema[key], fn, nextPath);
170
+ iterateSchema(schema[key], fn, nextPath);
171
171
  });
172
172
  }
173
173
 
@@ -871,11 +871,11 @@ Manager.prototype.setupFunctions = function (exporter, options) {
871
871
  .auth.user()
872
872
  .onDelete(async (user, context) => self._process((new (require(`${core}/events/auth/on-delete.js`))()).init(self, { user: user, context: context})));
873
873
 
874
- exporter.bm_subOnWrite =
874
+ exporter.bm_notificationsOnWrite =
875
875
  self.libraries.functions
876
876
  .runWith({memory: '256MB', timeoutSeconds: 60})
877
- .firestore.document('notifications/subscriptions/all/{token}')
878
- .onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/on-subscription.js`))()).init(self, { change: change, context: context, })));
877
+ .firestore.document('notifications/{token}')
878
+ .onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/notifications/on-write.js`))()).init(self, { change: change, context: context, })));
879
879
 
880
880
  // Setup cron jobs
881
881
  exporter.bm_cronDaily =
@@ -126,24 +126,24 @@ describe("BackendManager Tests", () => {
126
126
  describe("notifications", () => {
127
127
  it("unauthenticated can subscribe", async () => {
128
128
  const db = auth(accounts.unauthenticated);
129
- const doc = db.doc(`notifications/subscriptions/all/token`);
129
+ const doc = db.doc(`notifications/token`);
130
130
  await firebase.assertSucceeds(doc.set({token: 'token'}));
131
131
  });
132
132
  it("authenticated can subscribe", async () => {
133
133
  const db = auth(accounts.regular);
134
- const doc = db.doc(`notifications/subscriptions/all/token`);
134
+ const doc = db.doc(`notifications/token`);
135
135
  await firebase.assertSucceeds(doc.set({token: 'token'}));
136
136
  });
137
137
 
138
138
  it("unauthenticated can read subscription by token", async () => {
139
139
  const db = auth(accounts.unauthenticated);
140
- const doc = db.doc(`notifications/subscriptions/all/token`);
140
+ const doc = db.doc(`notifications/token`);
141
141
  await firebase.assertSucceeds(doc.get());
142
142
  });
143
143
 
144
144
  it("authenticated can read subscription by token", async () => {
145
145
  const db = auth(accounts.regular);
146
- const doc = db.doc(`notifications/subscriptions/all/token`);
146
+ const doc = db.doc(`notifications/token`);
147
147
  await firebase.assertSucceeds(doc.get());
148
148
  });
149
149
 
@@ -9,7 +9,7 @@ service cloud.firestore {
9
9
  match /{document=**} {
10
10
  allow read, write: if isAdmin();
11
11
  }
12
-
12
+
13
13
  // Protect user account data
14
14
  match /users/{uid} {
15
15
  allow read: if belongsTo(uid);
@@ -17,8 +17,8 @@ service cloud.firestore {
17
17
  }
18
18
 
19
19
  // Protect notification data
20
- match /notifications/subscriptions/all/{token} {
21
- allow read: if existingData().token == token || belongsTo(existingData().link.user.pk);
20
+ match /notifications/{token} {
21
+ allow read: if existingData().token == token || belongsTo(existingData().owner.uid);
22
22
  allow update: if existingData().token == token;
23
23
  allow create: if true;
24
24
  }
@@ -40,7 +40,7 @@ service cloud.firestore {
40
40
  }
41
41
  function belongsTo(identity) {
42
42
  return isAuthenticated() && (authUid() == identity || authEmail() == identity);
43
- // eventually include a check for (existingData().link.email == identity)...(in case its a doc owned by a user that's not actually user doc)
43
+ // eventually include a check for (existingData().owner.uid == identity)...(in case its a doc owned by a user that's not actually user doc)
44
44
  }
45
45
 
46
46
  function getRoles() {