@dereekb/firebase-server 12.7.0 → 13.0.1
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 +1 -1
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +2859 -0
- package/index.cjs.mjs +2 -0
- package/index.esm.js +2699 -0
- package/mailgun/index.cjs.default.js +1 -0
- package/mailgun/{src/lib/auth.mailgun.js → index.cjs.js} +6 -6
- package/mailgun/index.cjs.mjs +2 -0
- package/mailgun/index.esm.js +39 -0
- package/mailgun/package.json +20 -5
- package/model/index.cjs.default.js +1 -0
- package/model/index.cjs.js +4767 -0
- package/model/index.cjs.mjs +2 -0
- package/model/index.d.ts +1 -0
- package/model/index.esm.js +4630 -0
- package/model/package.json +28 -4
- package/model/src/lib/notification/notification.module.d.ts +10 -10
- package/model/src/lib/storagefile/storagefile.task.service.handler.d.ts +3 -14
- package/package.json +49 -47
- package/src/lib/auth/auth.context.d.ts +1 -1
- package/src/lib/auth/auth.service.d.ts +1 -1
- package/src/lib/function/assert.d.ts +2 -2
- package/src/lib/function/context.d.ts +1 -1
- package/src/lib/function/error.d.ts +0 -8
- package/src/lib/index.d.ts +1 -0
- package/src/lib/nest/app.d.ts +1 -1
- package/src/lib/nest/development/development.app.function.d.ts +1 -2
- package/src/lib/nest/function/call.d.ts +1 -1
- package/src/lib/nest/function/context.d.ts +0 -4
- package/src/lib/nest/function/index.d.ts +0 -1
- package/src/lib/nest/function/nest.d.ts +1 -1
- package/src/lib/nest/function/v2/blocking.d.ts +3 -2
- package/src/lib/nest/model/crud.assert.function.d.ts +0 -4
- package/src/lib/storage/storage.d.ts +1 -1
- package/src/lib/type.d.ts +9 -0
- package/test/index.cjs.default.js +1 -0
- package/test/index.cjs.js +1258 -0
- package/test/index.cjs.mjs +2 -0
- package/test/index.d.ts +1 -0
- package/test/index.esm.js +1181 -0
- package/test/package.json +31 -4
- package/test/src/lib/firebase/firebase.admin.auth.d.ts +22 -13
- package/test/src/lib/firebase/firebase.admin.collection.d.ts +6 -6
- package/test/src/lib/firebase/firebase.admin.d.ts +10 -10
- package/test/src/lib/firebase/firebase.admin.function.d.ts +9 -9
- package/test/src/lib/firebase/firebase.admin.nest.d.ts +8 -8
- package/test/src/lib/firebase/firebase.admin.nest.function.d.ts +9 -9
- package/test/src/lib/firebase/firebase.test.d.ts +30 -0
- package/test/src/lib/firebase/index.d.ts +1 -1
- package/test/src/lib/firestore/firestore.admin.d.ts +3 -3
- package/test/src/lib/firestore/firestore.d.ts +2 -2
- package/test/src/lib/storage/storage.admin.d.ts +3 -3
- package/test/src/lib/storage/storage.d.ts +2 -2
- package/zoho/LICENSE +1 -1
- package/zoho/index.cjs.default.js +1 -0
- package/zoho/index.cjs.js +82 -85
- package/zoho/index.cjs.mjs +2 -0
- package/zoho/index.d.ts +1 -0
- package/zoho/index.esm.js +84 -87
- package/zoho/package.json +15 -15
- package/CHANGELOG.md +0 -2237
- package/mailgun/src/index.js +0 -5
- package/mailgun/src/index.js.map +0 -1
- package/mailgun/src/lib/auth.mailgun.js.map +0 -1
- package/mailgun/src/lib/index.js +0 -5
- package/mailgun/src/lib/index.js.map +0 -1
- package/model/src/index.js +0 -5
- package/model/src/index.js.map +0 -1
- package/model/src/lib/index.js +0 -7
- package/model/src/lib/index.js.map +0 -1
- package/model/src/lib/mailgun/index.js +0 -5
- package/model/src/lib/mailgun/index.js.map +0 -1
- package/model/src/lib/mailgun/notification.send.service.mailgun.js +0 -68
- package/model/src/lib/mailgun/notification.send.service.mailgun.js.map +0 -1
- package/model/src/lib/notification/index.js +0 -20
- package/model/src/lib/notification/index.js.map +0 -1
- package/model/src/lib/notification/notification.action.init.service.js +0 -230
- package/model/src/lib/notification/notification.action.init.service.js.map +0 -1
- package/model/src/lib/notification/notification.action.service.js +0 -1487
- package/model/src/lib/notification/notification.action.service.js.map +0 -1
- package/model/src/lib/notification/notification.config.js +0 -13
- package/model/src/lib/notification/notification.config.js.map +0 -1
- package/model/src/lib/notification/notification.config.service.js +0 -60
- package/model/src/lib/notification/notification.config.service.js.map +0 -1
- package/model/src/lib/notification/notification.create.run.js +0 -59
- package/model/src/lib/notification/notification.create.run.js.map +0 -1
- package/model/src/lib/notification/notification.error.js +0 -87
- package/model/src/lib/notification/notification.error.js.map +0 -1
- package/model/src/lib/notification/notification.expedite.service.js +0 -112
- package/model/src/lib/notification/notification.expedite.service.js.map +0 -1
- package/model/src/lib/notification/notification.module.js +0 -106
- package/model/src/lib/notification/notification.module.js.map +0 -1
- package/model/src/lib/notification/notification.send.js +0 -3
- package/model/src/lib/notification/notification.send.js.map +0 -1
- package/model/src/lib/notification/notification.send.service.js +0 -10
- package/model/src/lib/notification/notification.send.service.js.map +0 -1
- package/model/src/lib/notification/notification.send.service.notificationsummary.js +0 -104
- package/model/src/lib/notification/notification.send.service.notificationsummary.js.map +0 -1
- package/model/src/lib/notification/notification.send.service.text.js +0 -29
- package/model/src/lib/notification/notification.send.service.text.js.map +0 -1
- package/model/src/lib/notification/notification.task.service.handler.js +0 -65
- package/model/src/lib/notification/notification.task.service.handler.js.map +0 -1
- package/model/src/lib/notification/notification.task.service.js +0 -10
- package/model/src/lib/notification/notification.task.service.js.map +0 -1
- package/model/src/lib/notification/notification.task.service.util.js +0 -27
- package/model/src/lib/notification/notification.task.service.util.js.map +0 -1
- package/model/src/lib/notification/notification.task.subtask.handler.js +0 -256
- package/model/src/lib/notification/notification.task.subtask.handler.js.map +0 -1
- package/model/src/lib/notification/notification.util.js +0 -478
- package/model/src/lib/notification/notification.util.js.map +0 -1
- package/model/src/lib/storagefile/index.js +0 -12
- package/model/src/lib/storagefile/index.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.action.init.service.js +0 -155
- package/model/src/lib/storagefile/storagefile.action.init.service.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.action.server.js +0 -797
- package/model/src/lib/storagefile/storagefile.action.server.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.error.js +0 -106
- package/model/src/lib/storagefile/storagefile.error.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.module.js +0 -64
- package/model/src/lib/storagefile/storagefile.module.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.task.service.handler.js +0 -287
- package/model/src/lib/storagefile/storagefile.task.service.handler.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.upload.service.initializer.js +0 -180
- package/model/src/lib/storagefile/storagefile.upload.service.initializer.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.upload.service.js +0 -10
- package/model/src/lib/storagefile/storagefile.upload.service.js.map +0 -1
- package/model/src/lib/storagefile/storagefile.util.js +0 -54
- package/model/src/lib/storagefile/storagefile.util.js.map +0 -1
- package/src/index.js +0 -5
- package/src/index.js.map +0 -1
- package/src/lib/auth/auth.context.js +0 -13
- package/src/lib/auth/auth.context.js.map +0 -1
- package/src/lib/auth/auth.service.error.js +0 -34
- package/src/lib/auth/auth.service.error.js.map +0 -1
- package/src/lib/auth/auth.service.js +0 -427
- package/src/lib/auth/auth.service.js.map +0 -1
- package/src/lib/auth/auth.util.js +0 -23
- package/src/lib/auth/auth.util.js.map +0 -1
- package/src/lib/auth/index.js +0 -8
- package/src/lib/auth/index.js.map +0 -1
- package/src/lib/env/env.service.js +0 -7
- package/src/lib/env/env.service.js.map +0 -1
- package/src/lib/env/index.js +0 -5
- package/src/lib/env/index.js.map +0 -1
- package/src/lib/firestore/array.js +0 -34
- package/src/lib/firestore/array.js.map +0 -1
- package/src/lib/firestore/driver.accessor.batch.js +0 -93
- package/src/lib/firestore/driver.accessor.batch.js.map +0 -1
- package/src/lib/firestore/driver.accessor.default.js +0 -62
- package/src/lib/firestore/driver.accessor.default.js.map +0 -1
- package/src/lib/firestore/driver.accessor.js +0 -50
- package/src/lib/firestore/driver.accessor.js.map +0 -1
- package/src/lib/firestore/driver.accessor.transaction.js +0 -96
- package/src/lib/firestore/driver.accessor.transaction.js.map +0 -1
- package/src/lib/firestore/driver.js +0 -14
- package/src/lib/firestore/driver.js.map +0 -1
- package/src/lib/firestore/driver.query.js +0 -55
- package/src/lib/firestore/driver.query.js.map +0 -1
- package/src/lib/firestore/firestore.js +0 -10
- package/src/lib/firestore/firestore.js.map +0 -1
- package/src/lib/firestore/increment.js +0 -17
- package/src/lib/firestore/increment.js.map +0 -1
- package/src/lib/firestore/index.js +0 -9
- package/src/lib/firestore/index.js.map +0 -1
- package/src/lib/function/assert.js +0 -68
- package/src/lib/function/assert.js.map +0 -1
- package/src/lib/function/context.js +0 -14
- package/src/lib/function/context.js.map +0 -1
- package/src/lib/function/error.auth.js +0 -25
- package/src/lib/function/error.auth.js.map +0 -1
- package/src/lib/function/error.js +0 -221
- package/src/lib/function/error.js.map +0 -1
- package/src/lib/function/index.js +0 -9
- package/src/lib/function/index.js.map +0 -1
- package/src/lib/function/type.js +0 -3
- package/src/lib/function/type.js.map +0 -1
- package/src/lib/index.js +0 -11
- package/src/lib/index.js.map +0 -1
- package/src/lib/nest/app.js +0 -114
- package/src/lib/nest/app.js.map +0 -1
- package/src/lib/nest/auth/auth.module.js +0 -60
- package/src/lib/nest/auth/auth.module.js.map +0 -1
- package/src/lib/nest/auth/auth.util.js +0 -72
- package/src/lib/nest/auth/auth.util.js.map +0 -1
- package/src/lib/nest/auth/index.js +0 -6
- package/src/lib/nest/auth/index.js.map +0 -1
- package/src/lib/nest/development/development.app.function.js +0 -38
- package/src/lib/nest/development/development.app.function.js.map +0 -1
- package/src/lib/nest/development/development.assert.function.js +0 -3
- package/src/lib/nest/development/development.assert.function.js.map +0 -1
- package/src/lib/nest/development/development.function.js +0 -41
- package/src/lib/nest/development/development.function.js.map +0 -1
- package/src/lib/nest/development/development.schedule.function.error.js +0 -35
- package/src/lib/nest/development/development.schedule.function.error.js.map +0 -1
- package/src/lib/nest/development/development.schedule.function.js +0 -54
- package/src/lib/nest/development/development.schedule.function.js.map +0 -1
- package/src/lib/nest/development/index.js +0 -9
- package/src/lib/nest/development/index.js.map +0 -1
- package/src/lib/nest/env/env.service.js +0 -19
- package/src/lib/nest/env/env.service.js.map +0 -1
- package/src/lib/nest/env/env.util.js +0 -12
- package/src/lib/nest/env/env.util.js.map +0 -1
- package/src/lib/nest/env/index.js +0 -6
- package/src/lib/nest/env/index.js.map +0 -1
- package/src/lib/nest/firebase/firebase.module.js +0 -17
- package/src/lib/nest/firebase/firebase.module.js.map +0 -1
- package/src/lib/nest/firebase/index.js +0 -5
- package/src/lib/nest/firebase/index.js.map +0 -1
- package/src/lib/nest/firestore/firestore.module.js +0 -86
- package/src/lib/nest/firestore/firestore.module.js.map +0 -1
- package/src/lib/nest/firestore/index.js +0 -5
- package/src/lib/nest/firestore/index.js.map +0 -1
- package/src/lib/nest/function/call.js +0 -46
- package/src/lib/nest/function/call.js.map +0 -1
- package/src/lib/nest/function/context.js +0 -79
- package/src/lib/nest/function/context.js.map +0 -1
- package/src/lib/nest/function/index.js +0 -10
- package/src/lib/nest/function/index.js.map +0 -1
- package/src/lib/nest/function/nest.js +0 -17
- package/src/lib/nest/function/nest.js.map +0 -1
- package/src/lib/nest/function/schedule.js +0 -8
- package/src/lib/nest/function/schedule.js.map +0 -1
- package/src/lib/nest/function/v1/call.d.ts +0 -59
- package/src/lib/nest/function/v1/call.js +0 -55
- package/src/lib/nest/function/v1/call.js.map +0 -1
- package/src/lib/nest/function/v1/event.d.ts +0 -80
- package/src/lib/nest/function/v1/event.js +0 -52
- package/src/lib/nest/function/v1/event.js.map +0 -1
- package/src/lib/nest/function/v1/index.d.ts +0 -3
- package/src/lib/nest/function/v1/index.js +0 -7
- package/src/lib/nest/function/v1/index.js.map +0 -1
- package/src/lib/nest/function/v1/schedule.d.ts +0 -47
- package/src/lib/nest/function/v1/schedule.js +0 -68
- package/src/lib/nest/function/v1/schedule.js.map +0 -1
- package/src/lib/nest/function/v2/blocking.js +0 -38
- package/src/lib/nest/function/v2/blocking.js.map +0 -1
- package/src/lib/nest/function/v2/call.js +0 -31
- package/src/lib/nest/function/v2/call.js.map +0 -1
- package/src/lib/nest/function/v2/event.js +0 -25
- package/src/lib/nest/function/v2/event.js.map +0 -1
- package/src/lib/nest/function/v2/index.js +0 -9
- package/src/lib/nest/function/v2/index.js.map +0 -1
- package/src/lib/nest/function/v2/schedule.js +0 -56
- package/src/lib/nest/function/v2/schedule.js.map +0 -1
- package/src/lib/nest/function/v2/taskqueue.js +0 -26
- package/src/lib/nest/function/v2/taskqueue.js.map +0 -1
- package/src/lib/nest/index.js +0 -15
- package/src/lib/nest/index.js.map +0 -1
- package/src/lib/nest/middleware/appcheck.decorator.js +0 -12
- package/src/lib/nest/middleware/appcheck.decorator.js.map +0 -1
- package/src/lib/nest/middleware/appcheck.js +0 -3
- package/src/lib/nest/middleware/appcheck.js.map +0 -1
- package/src/lib/nest/middleware/appcheck.middleware.js +0 -74
- package/src/lib/nest/middleware/appcheck.middleware.js.map +0 -1
- package/src/lib/nest/middleware/appcheck.module.js +0 -21
- package/src/lib/nest/middleware/appcheck.module.js.map +0 -1
- package/src/lib/nest/middleware/globalprefix.js +0 -11
- package/src/lib/nest/middleware/globalprefix.js.map +0 -1
- package/src/lib/nest/middleware/index.js +0 -10
- package/src/lib/nest/middleware/index.js.map +0 -1
- package/src/lib/nest/middleware/rawbody.middleware.js +0 -16
- package/src/lib/nest/middleware/rawbody.middleware.js.map +0 -1
- package/src/lib/nest/middleware/webhook.js +0 -24
- package/src/lib/nest/middleware/webhook.js.map +0 -1
- package/src/lib/nest/model/call.model.function.js +0 -73
- package/src/lib/nest/model/call.model.function.js.map +0 -1
- package/src/lib/nest/model/create.model.function.js +0 -27
- package/src/lib/nest/model/create.model.function.js.map +0 -1
- package/src/lib/nest/model/crud.assert.function.js +0 -3
- package/src/lib/nest/model/crud.assert.function.js.map +0 -1
- package/src/lib/nest/model/delete.model.function.js +0 -27
- package/src/lib/nest/model/delete.model.function.js.map +0 -1
- package/src/lib/nest/model/index.js +0 -11
- package/src/lib/nest/model/index.js.map +0 -1
- package/src/lib/nest/model/permission.error.js +0 -24
- package/src/lib/nest/model/permission.error.js.map +0 -1
- package/src/lib/nest/model/read.model.function.js +0 -27
- package/src/lib/nest/model/read.model.function.js.map +0 -1
- package/src/lib/nest/model/specifier.function.js +0 -35
- package/src/lib/nest/model/specifier.function.js.map +0 -1
- package/src/lib/nest/model/update.model.function.js +0 -27
- package/src/lib/nest/model/update.model.function.js.map +0 -1
- package/src/lib/nest/nest.provider.js +0 -89
- package/src/lib/nest/nest.provider.js.map +0 -1
- package/src/lib/nest/storage/index.js +0 -5
- package/src/lib/nest/storage/index.js.map +0 -1
- package/src/lib/nest/storage/storage.module.js +0 -112
- package/src/lib/nest/storage/storage.module.js.map +0 -1
- package/src/lib/storage/driver.accessor.js +0 -299
- package/src/lib/storage/driver.accessor.js.map +0 -1
- package/src/lib/storage/driver.js +0 -12
- package/src/lib/storage/driver.js.map +0 -1
- package/src/lib/storage/index.js +0 -8
- package/src/lib/storage/index.js.map +0 -1
- package/src/lib/storage/storage.js +0 -20
- package/src/lib/storage/storage.js.map +0 -1
- package/src/lib/storage/storage.service.js +0 -26
- package/src/lib/storage/storage.service.js.map +0 -1
- package/test/src/index.js +0 -5
- package/test/src/index.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.auth.js +0 -260
- package/test/src/lib/firebase/firebase.admin.auth.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.collection.js +0 -108
- package/test/src/lib/firebase/firebase.admin.collection.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.function.js +0 -132
- package/test/src/lib/firebase/firebase.admin.function.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.js +0 -174
- package/test/src/lib/firebase/firebase.admin.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.nest.function.callable.context.js +0 -42
- package/test/src/lib/firebase/firebase.admin.nest.function.callable.context.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.nest.function.cloud.context.js +0 -40
- package/test/src/lib/firebase/firebase.admin.nest.function.cloud.context.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.nest.function.js +0 -64
- package/test/src/lib/firebase/firebase.admin.nest.function.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.nest.js +0 -107
- package/test/src/lib/firebase/firebase.admin.nest.js.map +0 -1
- package/test/src/lib/firebase/firebase.admin.test.server.js +0 -37
- package/test/src/lib/firebase/firebase.admin.test.server.js.map +0 -1
- package/test/src/lib/firebase/firebase.function.js +0 -58
- package/test/src/lib/firebase/firebase.function.js.map +0 -1
- package/test/src/lib/firebase/firebase.jest.d.ts +0 -21
- package/test/src/lib/firebase/firebase.jest.js +0 -45
- package/test/src/lib/firebase/firebase.jest.js.map +0 -1
- package/test/src/lib/firebase/firebase.js +0 -74
- package/test/src/lib/firebase/firebase.js.map +0 -1
- package/test/src/lib/firebase/index.js +0 -15
- package/test/src/lib/firebase/index.js.map +0 -1
- package/test/src/lib/firestore/firestore.admin.js +0 -21
- package/test/src/lib/firestore/firestore.admin.js.map +0 -1
- package/test/src/lib/firestore/firestore.js +0 -57
- package/test/src/lib/firestore/firestore.js.map +0 -1
- package/test/src/lib/firestore/index.js +0 -6
- package/test/src/lib/firestore/index.js.map +0 -1
- package/test/src/lib/index.js +0 -7
- package/test/src/lib/index.js.map +0 -1
- package/test/src/lib/storage/index.js +0 -6
- package/test/src/lib/storage/index.js.map +0 -1
- package/test/src/lib/storage/storage.admin.js +0 -21
- package/test/src/lib/storage/storage.admin.js.map +0 -1
- package/test/src/lib/storage/storage.js +0 -59
- package/test/src/lib/storage/storage.js.map +0 -1
- /package/{zoho/index.cjs.d.ts → index.d.ts} +0 -0
- /package/{zoho/index.esm.d.ts → mailgun/index.d.ts} +0 -0
|
@@ -0,0 +1,4767 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var util = require('@dereekb/util');
|
|
4
|
+
var date = require('@dereekb/date');
|
|
5
|
+
var firebase = require('@dereekb/firebase');
|
|
6
|
+
var firebaseServer = require('@dereekb/firebase-server');
|
|
7
|
+
var dateFns = require('date-fns');
|
|
8
|
+
var common = require('@nestjs/common');
|
|
9
|
+
var config = require('@nestjs/config');
|
|
10
|
+
var makeError = require('make-error');
|
|
11
|
+
var archiver = require('archiver');
|
|
12
|
+
|
|
13
|
+
const MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST = 50;
|
|
14
|
+
function mailgunNotificationEmailSendService(config) {
|
|
15
|
+
const { mailgunService, defaultSendTemplateName, maxBatchSizePerRequest: inputMaxBatchSizePerRequest, messageBuilders: inputMessageBuilders } = config;
|
|
16
|
+
const lowercaseKeysMessageBuilders = util.mapObjectKeysToLowercase(inputMessageBuilders);
|
|
17
|
+
const maxBatchSizePerRequest = inputMaxBatchSizePerRequest ?? MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST;
|
|
18
|
+
const sendService = {
|
|
19
|
+
async buildSendInstanceForEmailNotificationMessages(notificationMessages) {
|
|
20
|
+
const templateMap = util.multiValueMapBuilder();
|
|
21
|
+
// group by templates
|
|
22
|
+
notificationMessages.forEach((x) => {
|
|
23
|
+
const sendTemplateName = x.emailContent?.sendTemplateName ?? x.content.sendTemplateName ?? defaultSendTemplateName;
|
|
24
|
+
if (sendTemplateName == null) {
|
|
25
|
+
throw new Error(`mailgunNotificationEmailSendService(): A sendTemplateName for a message was not available and no default was provided. Consider configuring a default send template.`);
|
|
26
|
+
}
|
|
27
|
+
templateMap.add(sendTemplateName, x);
|
|
28
|
+
});
|
|
29
|
+
// build send batches
|
|
30
|
+
const messageSendBatches = templateMap.entries().flatMap(([templateType, messages]) => {
|
|
31
|
+
return util.batch(messages, maxBatchSizePerRequest).map((x) => [templateType, x]);
|
|
32
|
+
});
|
|
33
|
+
// create the template requests
|
|
34
|
+
const templateRequestArrays = await Promise.all(messageSendBatches.map(async ([sendTemplateName, messages]) => {
|
|
35
|
+
const sendTemplateNameToLowercase = sendTemplateName.toLowerCase();
|
|
36
|
+
const builderForKey = lowercaseKeysMessageBuilders[sendTemplateNameToLowercase];
|
|
37
|
+
if (!builderForKey) {
|
|
38
|
+
throw new Error(`mailgunNotificationEmailSendService(): A template builder was not available for template type "${sendTemplateName}".`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const input = { mailgunService, sendTemplateName, messages };
|
|
42
|
+
return builderForKey(input);
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
const templateRequests = templateRequestArrays.flat();
|
|
46
|
+
const sendFn = async () => {
|
|
47
|
+
const success = [];
|
|
48
|
+
const failed = [];
|
|
49
|
+
// send the template emails
|
|
50
|
+
await util.runAsyncTasksForValues(templateRequests, (x) => {
|
|
51
|
+
const recipients = util.asArray(x.to).map((z) => z.email);
|
|
52
|
+
return mailgunService
|
|
53
|
+
.sendTemplateEmail(x)
|
|
54
|
+
.then(() => {
|
|
55
|
+
util.pushArrayItemsIntoArray(success, recipients);
|
|
56
|
+
})
|
|
57
|
+
.catch((e) => {
|
|
58
|
+
util.pushArrayItemsIntoArray(failed, recipients);
|
|
59
|
+
console.error('mailgunNotificationEmailSendService(): failed sending template emails', e);
|
|
60
|
+
// suppress error
|
|
61
|
+
});
|
|
62
|
+
}, { maxParallelTasks: 3 });
|
|
63
|
+
const result = {
|
|
64
|
+
success,
|
|
65
|
+
failed,
|
|
66
|
+
ignored: []
|
|
67
|
+
};
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
return sendFn;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return sendService;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createNotificationIdRequiredError() {
|
|
77
|
+
return firebaseServer.preconditionConflictError({
|
|
78
|
+
message: `The required id was not present when attempting to create a Notification.`,
|
|
79
|
+
code: firebase.CREATE_NOTIFICATION_ID_REQUIRED_ERROR_CODE
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function notificationModelAlreadyInitializedError() {
|
|
83
|
+
return firebaseServer.preconditionConflictError({
|
|
84
|
+
message: `This model has already been initialized.`,
|
|
85
|
+
code: firebase.NOTIFICATION_MODEL_ALREADY_INITIALIZED_ERROR_CODE
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function notificationBoxUnregistredModelTypeInitializationError(key) {
|
|
89
|
+
return firebaseServer.preconditionConflictError({
|
|
90
|
+
message: `This NotificationBox is associated with an unregistered model type.`,
|
|
91
|
+
code: firebase.NOTIFICATION_MODEL_ALREADY_INITIALIZED_ERROR_CODE,
|
|
92
|
+
data: {
|
|
93
|
+
key
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function notificationBoxDoesNotExist() {
|
|
98
|
+
return firebaseServer.preconditionConflictError({
|
|
99
|
+
message: `A NotificationBox does not exist for this model.`,
|
|
100
|
+
code: firebase.NOTIFICATION_BOX_DOES_NOT_EXIST_ERROR_CODE
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function notificationBoxExclusionTargetInvalidError() {
|
|
104
|
+
return firebaseServer.preconditionConflictError({
|
|
105
|
+
message: `The target for exclusion is invalid. The target recipient on the NotificationBox must be exist on the NotificationBox and have a uid to be excluded.`,
|
|
106
|
+
code: firebase.NOTIFICATION_BOX_EXCLUSION_TARGET_INVALID_ERROR_CODE
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function notificationBoxExistsForModelError() {
|
|
110
|
+
return firebaseServer.preconditionConflictError({
|
|
111
|
+
message: `A NotificationBox already exists for this model.`,
|
|
112
|
+
code: firebase.NOTIFICATION_BOX_EXISTS_FOR_MODEL_ERROR_CODE
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function notificationBoxRecipientDoesNotExistsError() {
|
|
116
|
+
return firebaseServer.preconditionConflictError({
|
|
117
|
+
message: `An existing NotificationBox recipient for the target does not exist. You must pass insert=true to create a new recipient.`,
|
|
118
|
+
code: firebase.NOTIFICATION_BOX_RECIPIENT_DOES_NOT_EXIST_ERROR_CODE
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function notificationUserInvalidUidForCreateError(uid) {
|
|
122
|
+
return firebaseServer.preconditionConflictError({
|
|
123
|
+
message: `The user with the uid '${uid}' does not exist. Cannot create a NotificationUser for them.`,
|
|
124
|
+
code: firebase.NOTIFICATION_USER_INVALID_UID_FOR_CREATE_ERROR_CODE,
|
|
125
|
+
data: {
|
|
126
|
+
uid
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function notificationUserBlockedFromBeingAddedToRecipientsError(uid) {
|
|
131
|
+
return firebaseServer.preconditionConflictError({
|
|
132
|
+
message: `The user with the uid '${uid}' has blocked themselves from from being added recipients.`,
|
|
133
|
+
code: firebase.NOTIFICATION_USER_BLOCKED_FROM_BEING_ADD_TO_RECIPIENTS_ERROR_CODE,
|
|
134
|
+
data: {
|
|
135
|
+
uid
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function notificationUserLockedConfigFromBeingUpdatedError(uid) {
|
|
140
|
+
return firebaseServer.preconditionConflictError({
|
|
141
|
+
message: `The user with the uid '${uid}' has locked their config from being updated.`,
|
|
142
|
+
code: firebase.NOTIFICATION_USER_LOCKED_CONFIG_FROM_BEING_UPDATED_ERROR_CODE,
|
|
143
|
+
data: {
|
|
144
|
+
uid
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: Create NotificationSummary
|
|
150
|
+
function makeNewNotificationSummaryTemplate(model) {
|
|
151
|
+
return {
|
|
152
|
+
cat: new Date(),
|
|
153
|
+
m: model,
|
|
154
|
+
o: firebase.firestoreDummyKey(),
|
|
155
|
+
s: true,
|
|
156
|
+
n: []
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* "Expands" the input into recipients for emails, texts, etc.
|
|
161
|
+
*
|
|
162
|
+
* Recipients may come from the NotificationBox, Notification or from the global recipients.
|
|
163
|
+
*
|
|
164
|
+
* Recipients are each configurable and may be defined with as little info as a single contact info, or have multiple contact info pieces associated with them.
|
|
165
|
+
*
|
|
166
|
+
* @param input
|
|
167
|
+
* @returns
|
|
168
|
+
*/
|
|
169
|
+
async function expandNotificationRecipients(input) {
|
|
170
|
+
const { notificationUserAccessor, authService, notification, notificationBox, globalRecipients: inputGlobalRecipients, recipientFlagOverride, notificationSummaryIdForUid: inputNotificationSummaryIdForUid, onlySendToExplicitlyEnabledRecipients: inputOnlySendToExplicitlyEnabledRecipients, onlyTextExplicitlyEnabledRecipients: inputOnlyTextExplicitlyEnabledRecipients } = input;
|
|
171
|
+
const notificationBoxId = notificationBox?.id;
|
|
172
|
+
const notificationSummaryIdForUid = inputNotificationSummaryIdForUid ?? (() => undefined);
|
|
173
|
+
const notificationTemplateType = notification.n.t || firebase.DEFAULT_NOTIFICATION_TEMPLATE_TYPE;
|
|
174
|
+
const recipientFlag = recipientFlagOverride ?? notification.rf ?? firebase.NotificationRecipientSendFlag.NORMAL;
|
|
175
|
+
const onlyTextExplicitlyEnabledRecipients = inputOnlyTextExplicitlyEnabledRecipients !== false; // defaults to true
|
|
176
|
+
const onlySendToExplicitlyEnabledRecipients = inputOnlySendToExplicitlyEnabledRecipients === true; // defaults to false
|
|
177
|
+
const onlyEmailExplicitlyEnabledRecipients = onlySendToExplicitlyEnabledRecipients;
|
|
178
|
+
const onlySendNotificationSummaryExplicitlyEnabledRecipients = onlySendToExplicitlyEnabledRecipients;
|
|
179
|
+
const { canSendToGlobalRecipients, canSendToBoxRecipients, canSendToExplicitRecipients } = firebase.allowedNotificationRecipients(recipientFlag);
|
|
180
|
+
const initialExplicitRecipients = canSendToExplicitRecipients ? notification.r : [];
|
|
181
|
+
const initialGlobalRecipients = canSendToGlobalRecipients && inputGlobalRecipients ? inputGlobalRecipients : [];
|
|
182
|
+
const explicitRecipients = initialExplicitRecipients.map((x) => ({
|
|
183
|
+
...x,
|
|
184
|
+
...firebase.effectiveNotificationBoxRecipientTemplateConfig(x)
|
|
185
|
+
}));
|
|
186
|
+
const globalRecipients = initialGlobalRecipients.map((x) => ({
|
|
187
|
+
...x,
|
|
188
|
+
...firebase.effectiveNotificationBoxRecipientTemplateConfig(x)
|
|
189
|
+
}));
|
|
190
|
+
const explicitAndGlobalRecipients = [...explicitRecipients, ...globalRecipients];
|
|
191
|
+
const allBoxRecipientConfigs = canSendToBoxRecipients && notificationBox ? notificationBox.r : [];
|
|
192
|
+
const recipientUids = new Set();
|
|
193
|
+
const relevantBoxRecipientConfigs = [];
|
|
194
|
+
// find all recipients in the NotificationBox with the target template type flagged for them.
|
|
195
|
+
allBoxRecipientConfigs.forEach((x) => {
|
|
196
|
+
// ignore opt-out flagged recipients and excluded recipients
|
|
197
|
+
if (!x.f && !x.x) {
|
|
198
|
+
const relevantConfig = x.c[notificationTemplateType];
|
|
199
|
+
const effectiveTemplateConfig = relevantConfig ? firebase.effectiveNotificationBoxRecipientTemplateConfig(relevantConfig) : undefined;
|
|
200
|
+
if (!effectiveTemplateConfig || effectiveTemplateConfig.st || effectiveTemplateConfig.se || effectiveTemplateConfig.sp || effectiveTemplateConfig.st) {
|
|
201
|
+
relevantBoxRecipientConfigs.push({
|
|
202
|
+
recipient: x,
|
|
203
|
+
effectiveTemplateConfig
|
|
204
|
+
});
|
|
205
|
+
if (x.uid) {
|
|
206
|
+
recipientUids.add(x.uid);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// add other recipients to the map
|
|
212
|
+
const nonNotificationBoxUidRecipientConfigs = new Map();
|
|
213
|
+
explicitAndGlobalRecipients.forEach((x) => {
|
|
214
|
+
const { uid } = x;
|
|
215
|
+
if (uid && !recipientUids.has(uid)) {
|
|
216
|
+
// if already in recipientUids then they are a box recipient and we don't have to try and load them.
|
|
217
|
+
nonNotificationBoxUidRecipientConfigs.set(uid, x);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const otherNotificationUserUidOptOuts = new Set();
|
|
221
|
+
const otherNotificationUserUidSendExclusions = new Set();
|
|
222
|
+
const notificationUserRecipientConfigs = new Map();
|
|
223
|
+
if (nonNotificationBoxUidRecipientConfigs.size > 0) {
|
|
224
|
+
const nonNotificationBoxRecipientUids = Array.from(nonNotificationBoxUidRecipientConfigs.keys());
|
|
225
|
+
const notificationUserDocuments = firebase.loadDocumentsForIds(notificationUserAccessor, nonNotificationBoxRecipientUids);
|
|
226
|
+
// Attempt to load the NotificationUser for each uid.
|
|
227
|
+
// Not guranteed to exist, but those that do we want to their configurations to decide opt-in/opt-out, as well as override the input recipient configuration for the Notification.
|
|
228
|
+
const notificationUsers = await firebase.getDocumentSnapshotDataPairsWithData(notificationUserDocuments);
|
|
229
|
+
notificationUsers.forEach((x) => {
|
|
230
|
+
const { data: notificationUser } = x;
|
|
231
|
+
const { x: exclusions, dc, gc } = notificationUser;
|
|
232
|
+
const canSendNotification = firebase.notificationSendExclusionCanSendFunction(exclusions);
|
|
233
|
+
const effectiveConfig = firebase.mergeNotificationUserDefaultNotificationBoxRecipientConfig(dc, gc);
|
|
234
|
+
const uid = x.document.id;
|
|
235
|
+
notificationUserRecipientConfigs.set(uid, effectiveConfig);
|
|
236
|
+
// check if flagged for opt out on the global/default config
|
|
237
|
+
if (effectiveConfig.f) {
|
|
238
|
+
// if flagged for opt out, add to set
|
|
239
|
+
otherNotificationUserUidOptOuts.add(uid);
|
|
240
|
+
}
|
|
241
|
+
const isAllowedToSend = notificationBoxId ? canSendNotification(notificationBoxId) : true;
|
|
242
|
+
if (!isAllowedToSend) {
|
|
243
|
+
otherNotificationUserUidSendExclusions.add(uid);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Other NotificationRecipientWithConfig
|
|
249
|
+
*/
|
|
250
|
+
const otherRecipientConfigs = new Map();
|
|
251
|
+
const explicitOtherRecipientEmailAddresses = new Map();
|
|
252
|
+
const explicitOtherRecipientTextNumbers = new Map();
|
|
253
|
+
const explicitOtherRecipientNotificationSummaryIds = new Map();
|
|
254
|
+
explicitAndGlobalRecipients.forEach((x) => {
|
|
255
|
+
const uid = x.uid;
|
|
256
|
+
if (uid) {
|
|
257
|
+
if (otherNotificationUserUidOptOuts.has(uid) || otherNotificationUserUidSendExclusions.has(uid)) {
|
|
258
|
+
return; // do not add to the recipients at all, user has opted out or send is excluded
|
|
259
|
+
}
|
|
260
|
+
const notificationUserRecipientConfig = notificationUserRecipientConfigs.get(uid);
|
|
261
|
+
if (notificationUserRecipientConfig != null) {
|
|
262
|
+
const userTemplateTypeConfig = notificationUserRecipientConfig.c[notificationTemplateType] ?? {};
|
|
263
|
+
const templateConfig = firebase.mergeNotificationBoxRecipientTemplateConfigs(firebase.effectiveNotificationBoxRecipientTemplateConfig(userTemplateTypeConfig), x);
|
|
264
|
+
// replace the input NotificationRecipientWithConfig with the user's config
|
|
265
|
+
x = {
|
|
266
|
+
...notificationUserRecipientConfig,
|
|
267
|
+
...firebase.effectiveNotificationBoxRecipientTemplateConfig(templateConfig),
|
|
268
|
+
uid
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
recipientUids.add(uid);
|
|
272
|
+
otherRecipientConfigs.set(uid, x);
|
|
273
|
+
}
|
|
274
|
+
if (x.e) {
|
|
275
|
+
explicitOtherRecipientEmailAddresses.set(x.e.toLowerCase(), x);
|
|
276
|
+
}
|
|
277
|
+
if (x.t) {
|
|
278
|
+
explicitOtherRecipientTextNumbers.set(x.t, x);
|
|
279
|
+
}
|
|
280
|
+
if (x.s) {
|
|
281
|
+
explicitOtherRecipientNotificationSummaryIds.set(x.s, x);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// load user details from auth service
|
|
285
|
+
const allUserDetails = await Promise.all(Array.from(recipientUids).map((uid) => authService
|
|
286
|
+
.userContext(uid)
|
|
287
|
+
.loadDetails()
|
|
288
|
+
.then((details) => [uid, details])
|
|
289
|
+
.catch(() => [uid, undefined])));
|
|
290
|
+
const userDetailsMap = new Map(allUserDetails);
|
|
291
|
+
const _internal = {
|
|
292
|
+
userDetailsMap,
|
|
293
|
+
explicitRecipients,
|
|
294
|
+
globalRecipients,
|
|
295
|
+
allBoxRecipientConfigs,
|
|
296
|
+
relevantBoxRecipientConfigs,
|
|
297
|
+
recipientUids,
|
|
298
|
+
otherRecipientConfigs,
|
|
299
|
+
explicitOtherRecipientEmailAddresses,
|
|
300
|
+
explicitOtherRecipientTextNumbers,
|
|
301
|
+
explicitOtherRecipientNotificationSummaryIds,
|
|
302
|
+
otherNotificationUserUidOptOuts,
|
|
303
|
+
otherNotificationUserUidSendExclusions,
|
|
304
|
+
nonNotificationBoxUidRecipientConfigs,
|
|
305
|
+
notificationUserRecipientConfigs
|
|
306
|
+
};
|
|
307
|
+
// make all email recipients
|
|
308
|
+
const emails = [];
|
|
309
|
+
const emailUidsSet = new Set();
|
|
310
|
+
function checkShouldSendEmail(sendEmailEnabled) {
|
|
311
|
+
return (!onlyEmailExplicitlyEnabledRecipients && sendEmailEnabled !== false) || (onlyEmailExplicitlyEnabledRecipients && sendEmailEnabled === true);
|
|
312
|
+
}
|
|
313
|
+
// start with all box recipients
|
|
314
|
+
relevantBoxRecipientConfigs.forEach((x) => {
|
|
315
|
+
const { recipient } = x;
|
|
316
|
+
const { uid, e: overrideRecipientEmail, n: overrideRecipientName } = recipient;
|
|
317
|
+
const userDetails = uid ? userDetailsMap.get(uid) : undefined;
|
|
318
|
+
const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
|
|
319
|
+
const sendEmailEnabled = x.effectiveTemplateConfig?.se;
|
|
320
|
+
const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
|
|
321
|
+
if (shouldSendEmail && !emailUidsSet.has(uid ?? '')) {
|
|
322
|
+
const e = overrideRecipientEmail ?? userDetails?.email; // use override email or the default email
|
|
323
|
+
if (e) {
|
|
324
|
+
const n = overrideRecipientName ?? userDetails?.displayName;
|
|
325
|
+
const emailAddress = e.toLowerCase();
|
|
326
|
+
explicitOtherRecipientEmailAddresses.delete(emailAddress); // don't double-send to the same email
|
|
327
|
+
const emailRecipient = {
|
|
328
|
+
emailAddress,
|
|
329
|
+
name: n,
|
|
330
|
+
boxRecipient: x,
|
|
331
|
+
otherRecipient: otherRecipientForUser
|
|
332
|
+
};
|
|
333
|
+
emails.push(emailRecipient);
|
|
334
|
+
if (uid) {
|
|
335
|
+
emailUidsSet.add(uid);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
otherRecipientConfigs.forEach((x, uid) => {
|
|
341
|
+
// add users who existing in the system at this step, then other recipients in the next step
|
|
342
|
+
const userDetails = userDetailsMap.get(uid);
|
|
343
|
+
if (userDetails) {
|
|
344
|
+
const { email: userEmailAddress, displayName } = userDetails;
|
|
345
|
+
const sendEmailEnabled = x.se;
|
|
346
|
+
const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
|
|
347
|
+
if (userEmailAddress && shouldSendEmail && !emailUidsSet.has(uid)) {
|
|
348
|
+
const emailAddress = userEmailAddress.toLowerCase();
|
|
349
|
+
const name = displayName || x.n;
|
|
350
|
+
const emailRecipient = {
|
|
351
|
+
emailAddress,
|
|
352
|
+
name,
|
|
353
|
+
otherRecipient: x
|
|
354
|
+
};
|
|
355
|
+
emails.push(emailRecipient);
|
|
356
|
+
emailUidsSet.add(uid);
|
|
357
|
+
explicitOtherRecipientEmailAddresses.delete(emailAddress);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
explicitOtherRecipientEmailAddresses.forEach((x, emailAddress) => {
|
|
362
|
+
const sendEmailEnabled = x.se;
|
|
363
|
+
const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
|
|
364
|
+
if (shouldSendEmail) {
|
|
365
|
+
const emailRecipient = {
|
|
366
|
+
emailAddress: emailAddress,
|
|
367
|
+
name: x.n,
|
|
368
|
+
otherRecipient: x
|
|
369
|
+
};
|
|
370
|
+
emails.push(emailRecipient);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
// make all text recipients
|
|
374
|
+
// text recipients should be explicitly enabled, or marked true
|
|
375
|
+
const texts = [];
|
|
376
|
+
const textUidsSet = new Set();
|
|
377
|
+
function checkShouldSendText(sendTextEnabled) {
|
|
378
|
+
return (onlyTextExplicitlyEnabledRecipients && sendTextEnabled === true) || (!onlyTextExplicitlyEnabledRecipients && sendTextEnabled !== false);
|
|
379
|
+
}
|
|
380
|
+
relevantBoxRecipientConfigs.forEach((x) => {
|
|
381
|
+
const { recipient } = x;
|
|
382
|
+
const { uid } = recipient;
|
|
383
|
+
const userDetails = uid ? userDetailsMap.get(uid) : undefined;
|
|
384
|
+
const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
|
|
385
|
+
// only send a text if explicitly enabled
|
|
386
|
+
const sendTextEnabled = x.effectiveTemplateConfig?.st;
|
|
387
|
+
const shouldSendText = checkShouldSendText(sendTextEnabled);
|
|
388
|
+
if (shouldSendText && !textUidsSet.has(uid ?? '')) {
|
|
389
|
+
const t = x.recipient.t ?? userDetails?.phoneNumber; // use override phoneNumber or the default phone
|
|
390
|
+
if (t) {
|
|
391
|
+
const name = userDetails?.displayName ?? x.recipient.n;
|
|
392
|
+
const phoneNumber = t;
|
|
393
|
+
explicitOtherRecipientTextNumbers.delete(phoneNumber); // don't double-send to the same text phone number
|
|
394
|
+
const textRecipient = {
|
|
395
|
+
phoneNumber,
|
|
396
|
+
name,
|
|
397
|
+
boxRecipient: x,
|
|
398
|
+
otherRecipient: otherRecipientForUser
|
|
399
|
+
};
|
|
400
|
+
texts.push(textRecipient);
|
|
401
|
+
if (uid) {
|
|
402
|
+
textUidsSet.add(uid);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
otherRecipientConfigs.forEach((x, uid) => {
|
|
408
|
+
// add users who existing in the system at this step, then other recipients in the next step
|
|
409
|
+
const userDetails = userDetailsMap.get(uid);
|
|
410
|
+
if (userDetails) {
|
|
411
|
+
const { phoneNumber, displayName } = userDetails;
|
|
412
|
+
const sendTextEnabled = x.st;
|
|
413
|
+
const sendText = checkShouldSendText(sendTextEnabled);
|
|
414
|
+
if (phoneNumber != null && sendText && !textUidsSet.has(uid)) {
|
|
415
|
+
const name = displayName || x.n;
|
|
416
|
+
const textRecipient = {
|
|
417
|
+
phoneNumber: phoneNumber,
|
|
418
|
+
name,
|
|
419
|
+
otherRecipient: x
|
|
420
|
+
};
|
|
421
|
+
texts.push(textRecipient);
|
|
422
|
+
textUidsSet.add(uid);
|
|
423
|
+
explicitOtherRecipientTextNumbers.delete(phoneNumber); // don't double-send to the same text phone number
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
explicitOtherRecipientTextNumbers.forEach((x, t) => {
|
|
428
|
+
const sendTextEnabled = x.st;
|
|
429
|
+
const shouldSendText = checkShouldSendText(sendTextEnabled);
|
|
430
|
+
if (shouldSendText) {
|
|
431
|
+
const textRecipient = {
|
|
432
|
+
phoneNumber: t,
|
|
433
|
+
name: x.n,
|
|
434
|
+
otherRecipient: x
|
|
435
|
+
};
|
|
436
|
+
texts.push(textRecipient);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
// TODO: Add push notification details...
|
|
440
|
+
// make all notification summary recipients
|
|
441
|
+
const notificationSummaries = [];
|
|
442
|
+
const notificationSummaryKeysSet = new Set();
|
|
443
|
+
const notificationSummaryUidsSet = new Set();
|
|
444
|
+
function checkShouldSendNotificationSummary(sendNotificationSummaryEnabled) {
|
|
445
|
+
return (!onlySendNotificationSummaryExplicitlyEnabledRecipients && sendNotificationSummaryEnabled !== false) || (onlySendNotificationSummaryExplicitlyEnabledRecipients && sendNotificationSummaryEnabled === true);
|
|
446
|
+
}
|
|
447
|
+
relevantBoxRecipientConfigs.forEach((x) => {
|
|
448
|
+
const { recipient } = x;
|
|
449
|
+
const { uid } = recipient;
|
|
450
|
+
const userDetails = uid ? userDetailsMap.get(uid) : undefined;
|
|
451
|
+
const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
|
|
452
|
+
const sendNotificationSummaryEnabled = x.effectiveTemplateConfig?.sn;
|
|
453
|
+
const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
|
|
454
|
+
if (shouldSendNotificationSummary) {
|
|
455
|
+
let notificationSummaryId;
|
|
456
|
+
if (uid) {
|
|
457
|
+
// only use the uid (and ignore recipient config) if uid is defined
|
|
458
|
+
notificationSummaryId = notificationSummaryIdForUid(uid);
|
|
459
|
+
notificationSummaryUidsSet.add(uid);
|
|
460
|
+
}
|
|
461
|
+
else if (x.recipient.s) {
|
|
462
|
+
notificationSummaryId = x.recipient.s;
|
|
463
|
+
}
|
|
464
|
+
if (notificationSummaryId) {
|
|
465
|
+
const name = userDetails?.displayName ?? x.recipient.n;
|
|
466
|
+
notificationSummaries.push({
|
|
467
|
+
notificationSummaryId,
|
|
468
|
+
boxRecipient: x,
|
|
469
|
+
otherRecipient: otherRecipientForUser,
|
|
470
|
+
name
|
|
471
|
+
});
|
|
472
|
+
explicitOtherRecipientNotificationSummaryIds.delete(notificationSummaryId); // don't double send
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
otherRecipientConfigs.forEach((x, uid) => {
|
|
477
|
+
const userDetails = userDetailsMap.get(uid);
|
|
478
|
+
if (userDetails) {
|
|
479
|
+
const { displayName } = userDetails;
|
|
480
|
+
const sendNotificationSummaryEnabled = x.sn;
|
|
481
|
+
const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
|
|
482
|
+
if (shouldSendNotificationSummary && !notificationSummaryUidsSet.has(uid ?? '')) {
|
|
483
|
+
let notificationSummaryId;
|
|
484
|
+
if (uid) {
|
|
485
|
+
notificationSummaryId = notificationSummaryIdForUid(uid);
|
|
486
|
+
notificationSummaryUidsSet.add(uid);
|
|
487
|
+
}
|
|
488
|
+
else if (x.s) {
|
|
489
|
+
notificationSummaryId = x.s;
|
|
490
|
+
}
|
|
491
|
+
if (notificationSummaryId) {
|
|
492
|
+
if (!notificationSummaryKeysSet.has(notificationSummaryId)) {
|
|
493
|
+
const name = displayName || x.n;
|
|
494
|
+
const notificationSummary = {
|
|
495
|
+
notificationSummaryId,
|
|
496
|
+
otherRecipient: x,
|
|
497
|
+
name
|
|
498
|
+
};
|
|
499
|
+
notificationSummaries.push(notificationSummary);
|
|
500
|
+
explicitOtherRecipientNotificationSummaryIds.delete(notificationSummaryId);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
explicitOtherRecipientNotificationSummaryIds.forEach((x, notificationSummaryId) => {
|
|
507
|
+
const sendNotificationSummaryEnabled = x.sn;
|
|
508
|
+
const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
|
|
509
|
+
if (shouldSendNotificationSummary) {
|
|
510
|
+
const notificationSummary = {
|
|
511
|
+
notificationSummaryId,
|
|
512
|
+
otherRecipient: x,
|
|
513
|
+
name: x.n
|
|
514
|
+
};
|
|
515
|
+
notificationSummaries.push(notificationSummary);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
// results
|
|
519
|
+
const result = {
|
|
520
|
+
_internal,
|
|
521
|
+
emails,
|
|
522
|
+
texts,
|
|
523
|
+
notificationSummaries
|
|
524
|
+
};
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
function updateNotificationUserNotificationBoxRecipientConfig(input) {
|
|
528
|
+
const { notificationBoxId, notificationUserId, notificationUser, insertingRecipientIntoNotificationBox, removeRecipientFromNotificationBox, notificationBoxRecipient } = input;
|
|
529
|
+
const currentNotificationUserBoxIndex = notificationUser.bc.findIndex((x) => x.nb === notificationBoxId);
|
|
530
|
+
const currentNotificationUserBoxIndexExists = currentNotificationUserBoxIndex !== -1;
|
|
531
|
+
const currentNotificationUserBoxGlobalConfig = notificationUser.gc;
|
|
532
|
+
const currentNotificationUserBoxConfig = notificationUser.bc[currentNotificationUserBoxIndex] ?? {};
|
|
533
|
+
/**
|
|
534
|
+
* If bc is updated then the user should be updated too
|
|
535
|
+
*/
|
|
536
|
+
let updatedBc;
|
|
537
|
+
let updatedNotificationBoxRecipient;
|
|
538
|
+
if (removeRecipientFromNotificationBox) {
|
|
539
|
+
// flag as removed in the NotificationUser details if not already flagged as such
|
|
540
|
+
if (currentNotificationUserBoxIndexExists && currentNotificationUserBoxConfig.rm !== true) {
|
|
541
|
+
updatedBc = [...notificationUser.bc];
|
|
542
|
+
updatedBc[currentNotificationUserBoxIndex] = {
|
|
543
|
+
...currentNotificationUserBoxConfig,
|
|
544
|
+
nb: notificationBoxId, // set the NotificationBox id
|
|
545
|
+
c: currentNotificationUserBoxConfig.c ?? {},
|
|
546
|
+
i: util.UNSET_INDEX_NUMBER, // index should be cleared and set to -1
|
|
547
|
+
ns: false, // sync'd
|
|
548
|
+
rm: true
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (notificationBoxRecipient != null) {
|
|
553
|
+
const { ns: currentConfigNeedsSync, lk: lockedFromChanges, bk: blockedFromAdd } = {
|
|
554
|
+
ns: currentNotificationUserBoxConfig.ns,
|
|
555
|
+
lk: currentNotificationUserBoxGlobalConfig.lk ?? currentNotificationUserBoxConfig.lk,
|
|
556
|
+
bk: currentNotificationUserBoxGlobalConfig.bk ?? currentNotificationUserBoxConfig.bk
|
|
557
|
+
};
|
|
558
|
+
// if we're re-inserting, then take the prevous config and restore as it was and remove the rm tag
|
|
559
|
+
let updateWithNotificationBoxRecipient;
|
|
560
|
+
if (insertingRecipientIntoNotificationBox) {
|
|
561
|
+
// does not exist in the NotificationBox currently
|
|
562
|
+
if (blockedFromAdd) {
|
|
563
|
+
throw notificationUserBlockedFromBeingAddedToRecipientsError(notificationUserId);
|
|
564
|
+
}
|
|
565
|
+
else if (lockedFromChanges) {
|
|
566
|
+
// ignored the notificationBoxRecipient's updates
|
|
567
|
+
updateWithNotificationBoxRecipient = currentNotificationUserBoxConfig;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
updateWithNotificationBoxRecipient = firebase.mergeNotificationBoxRecipients(notificationBoxRecipient, currentNotificationUserBoxConfig);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
// if locked from changes, throw error
|
|
575
|
+
if (lockedFromChanges) {
|
|
576
|
+
throw notificationUserLockedConfigFromBeingUpdatedError(notificationUserId);
|
|
577
|
+
}
|
|
578
|
+
else if (currentConfigNeedsSync) {
|
|
579
|
+
// if needs sync, then merge changes from the config into the notificationBoxRecipient
|
|
580
|
+
updateWithNotificationBoxRecipient = firebase.mergeNotificationBoxRecipients(notificationBoxRecipient, currentNotificationUserBoxConfig);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
// use as-is
|
|
584
|
+
updateWithNotificationBoxRecipient = notificationBoxRecipient;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const updatedNotificationUserBoxEntry = firebase.mergeNotificationUserNotificationBoxRecipientConfigs({
|
|
588
|
+
...currentNotificationUserBoxConfig,
|
|
589
|
+
i: notificationBoxRecipient.i,
|
|
590
|
+
c: currentNotificationUserBoxConfig.c ?? {},
|
|
591
|
+
nb: notificationBoxId, // set the NotificationBox id
|
|
592
|
+
rm: false // remove/clear the removed flag
|
|
593
|
+
}, updateWithNotificationBoxRecipient);
|
|
594
|
+
updatedBc = [...notificationUser.bc];
|
|
595
|
+
if (currentNotificationUserBoxIndexExists) {
|
|
596
|
+
updatedBc[currentNotificationUserBoxIndex] = updatedNotificationUserBoxEntry;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
updatedBc.push(updatedNotificationUserBoxEntry);
|
|
600
|
+
}
|
|
601
|
+
// re-apply exclusions to the updated config(s)
|
|
602
|
+
const withExclusions = firebase.applyExclusionsToNotificationUserNotificationBoxRecipientConfigs({
|
|
603
|
+
notificationUser,
|
|
604
|
+
bc: updatedBc,
|
|
605
|
+
recalculateNs: false
|
|
606
|
+
});
|
|
607
|
+
updatedBc = withExclusions.bc;
|
|
608
|
+
// sync index with input NotificationBoxRecipient
|
|
609
|
+
updatedNotificationUserBoxEntry.i = notificationBoxRecipient.i;
|
|
610
|
+
updatedNotificationBoxRecipient = updatedNotificationUserBoxEntry;
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
updatedBc,
|
|
614
|
+
updatedNotificationBoxRecipient
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Removes the completed checkpoints from the inputCompletions array based on the handleTaskResult.
|
|
620
|
+
*
|
|
621
|
+
* @param inputCompletions
|
|
622
|
+
* @param handleTaskResult
|
|
623
|
+
* @returns
|
|
624
|
+
*/
|
|
625
|
+
function removeFromCompletionsArrayWithTaskResult(inputCompletions, handleTaskResult) {
|
|
626
|
+
const { removeAllCompletedCheckpoints, removeFromCompletedCheckpoints } = handleTaskResult;
|
|
627
|
+
let result;
|
|
628
|
+
if (removeAllCompletedCheckpoints) {
|
|
629
|
+
result = [];
|
|
630
|
+
}
|
|
631
|
+
else if (removeFromCompletedCheckpoints != null) {
|
|
632
|
+
const removeFromCompletionsSet = new Set(util.asArray(removeFromCompletedCheckpoints));
|
|
633
|
+
result = inputCompletions.filter((x) => !removeFromCompletionsSet.has(x));
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
result = inputCompletions;
|
|
637
|
+
}
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Injection token for the BaseNotificationServerActionsContext
|
|
643
|
+
*/
|
|
644
|
+
const BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = 'BASE_NOTIFICATION_SERVER_ACTION_CONTEXT';
|
|
645
|
+
/**
|
|
646
|
+
* Injection token for the NotificationServerActionsContext
|
|
647
|
+
*/
|
|
648
|
+
const NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = 'NOTIFICATION_SERVER_ACTION_CONTEXT';
|
|
649
|
+
class NotificationServerActions {
|
|
650
|
+
}
|
|
651
|
+
function notificationServerActions(context) {
|
|
652
|
+
return {
|
|
653
|
+
createNotificationUser: createNotificationUserFactory(context),
|
|
654
|
+
updateNotificationUser: updateNotificationUserFactory(context),
|
|
655
|
+
resyncNotificationUser: resyncNotificationUserFactory(context),
|
|
656
|
+
resyncAllNotificationUsers: resyncAllNotificationUsersFactory(context),
|
|
657
|
+
createNotificationSummary: createNotificationSummaryFactory(context),
|
|
658
|
+
updateNotificationSummary: updateNotificationSummaryFactory(context),
|
|
659
|
+
createNotificationBox: createNotificationBoxFactory(context),
|
|
660
|
+
updateNotificationBox: updateNotificationBoxFactory(context),
|
|
661
|
+
updateNotificationBoxRecipient: updateNotificationBoxRecipientFactory(context),
|
|
662
|
+
sendNotification: sendNotificationFactory(context),
|
|
663
|
+
sendQueuedNotifications: sendQueuedNotificationsFactory(context),
|
|
664
|
+
cleanupSentNotifications: cleanupSentNotificationsFactory(context)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
// MARK: Actions
|
|
668
|
+
function createNotificationUserFactory(context) {
|
|
669
|
+
const { firebaseServerActionTransformFunctionFactory, notificationUserCollection, authService } = context;
|
|
670
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CreateNotificationUserParams, async (params) => {
|
|
671
|
+
const { uid } = params;
|
|
672
|
+
return async () => {
|
|
673
|
+
// assert they exist in the auth system
|
|
674
|
+
const userContext = authService.userContext(uid);
|
|
675
|
+
const userExistsInAuth = await userContext.exists();
|
|
676
|
+
if (!userExistsInAuth) {
|
|
677
|
+
throw notificationUserInvalidUidForCreateError(uid);
|
|
678
|
+
}
|
|
679
|
+
const notificationUserDocument = notificationUserCollection.documentAccessor().loadDocumentForId(uid);
|
|
680
|
+
const newUserTemplate = {
|
|
681
|
+
uid,
|
|
682
|
+
x: [],
|
|
683
|
+
bc: [],
|
|
684
|
+
b: [],
|
|
685
|
+
dc: {
|
|
686
|
+
c: {}
|
|
687
|
+
},
|
|
688
|
+
gc: {
|
|
689
|
+
c: {}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
await notificationUserDocument.create(newUserTemplate);
|
|
693
|
+
return notificationUserDocument;
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function updateNotificationUserFactory(context) {
|
|
698
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationUserCollection, appNotificationTemplateTypeInfoRecordService } = context;
|
|
699
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateNotificationUserParams, async (params) => {
|
|
700
|
+
const { gc: inputGc, dc: inputDc, bc: inputBc } = params;
|
|
701
|
+
return async (notificationUserDocument) => {
|
|
702
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
703
|
+
const notificationUserDocumentInTransaction = notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationUserDocument);
|
|
704
|
+
const notificationUser = await firebaseServer.assertSnapshotData(notificationUserDocumentInTransaction);
|
|
705
|
+
const updateTemplate = {};
|
|
706
|
+
const allKnownNotificationTypes = appNotificationTemplateTypeInfoRecordService.getAllKnownTemplateTypes();
|
|
707
|
+
if (inputDc != null) {
|
|
708
|
+
updateTemplate.dc = firebase.updateNotificationUserDefaultNotificationBoxRecipientConfig(notificationUser.dc, inputDc, allKnownNotificationTypes);
|
|
709
|
+
}
|
|
710
|
+
if (inputGc != null) {
|
|
711
|
+
const nextGc = firebase.updateNotificationUserDefaultNotificationBoxRecipientConfig(notificationUser.gc, inputGc, allKnownNotificationTypes);
|
|
712
|
+
if (!util.areEqualPOJOValues(notificationUser.gc, nextGc)) {
|
|
713
|
+
updateTemplate.gc = nextGc;
|
|
714
|
+
// iterate and update any box config that has the effective recipient change
|
|
715
|
+
updateTemplate.bc = notificationUser.bc.map((currentConfig) => {
|
|
716
|
+
// check item isn't already marked for sync or marked as removed
|
|
717
|
+
if (currentConfig.ns === true || currentConfig.rm === true) {
|
|
718
|
+
return currentConfig;
|
|
719
|
+
}
|
|
720
|
+
const currentEffectiveRecipient = firebase.effectiveNotificationBoxRecipientConfig({
|
|
721
|
+
uid: notificationUser.uid,
|
|
722
|
+
appNotificationTemplateTypeInfoRecordService,
|
|
723
|
+
gc: notificationUser.gc,
|
|
724
|
+
boxConfig: currentConfig
|
|
725
|
+
});
|
|
726
|
+
const nextEffectiveRecipient = firebase.effectiveNotificationBoxRecipientConfig({
|
|
727
|
+
uid: notificationUser.uid,
|
|
728
|
+
appNotificationTemplateTypeInfoRecordService,
|
|
729
|
+
gc: nextGc,
|
|
730
|
+
boxConfig: currentConfig
|
|
731
|
+
});
|
|
732
|
+
const effectiveConfigChanged = !util.areEqualPOJOValues(currentEffectiveRecipient, nextEffectiveRecipient);
|
|
733
|
+
return effectiveConfigChanged ? { ...currentConfig, ns: true } : currentConfig;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (inputBc != null) {
|
|
738
|
+
const updateTemplateBc = firebase.updateNotificationUserNotificationBoxRecipientConfigs(updateTemplate.bc ?? notificationUser.bc, inputBc, appNotificationTemplateTypeInfoRecordService);
|
|
739
|
+
if (updateTemplateBc != null) {
|
|
740
|
+
// re-apply exclusions to the updated configs
|
|
741
|
+
const withExclusions = firebase.applyExclusionsToNotificationUserNotificationBoxRecipientConfigs({
|
|
742
|
+
notificationUser,
|
|
743
|
+
bc: updateTemplateBc,
|
|
744
|
+
recalculateNs: false
|
|
745
|
+
});
|
|
746
|
+
updateTemplate.bc = withExclusions.bc;
|
|
747
|
+
updateTemplate.b = updateTemplateBc.map((x) => x.nb);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// if bc is being updated, then also update ns
|
|
751
|
+
if (updateTemplate.bc != null) {
|
|
752
|
+
updateTemplate.ns = firebase.calculateNsForNotificationUserNotificationBoxRecipientConfigs(updateTemplate.bc);
|
|
753
|
+
}
|
|
754
|
+
await notificationUserDocumentInTransaction.update(updateTemplate);
|
|
755
|
+
});
|
|
756
|
+
return notificationUserDocument;
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const MAX_NOTIFICATION_BOXES_TO_UPDATE_PER_BATCH = 50;
|
|
761
|
+
function resyncNotificationUserFactory(context) {
|
|
762
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationBoxCollection, notificationUserCollection, appNotificationTemplateTypeInfoRecordService } = context;
|
|
763
|
+
return firebaseServerActionTransformFunctionFactory(firebase.ResyncNotificationUserParams, async () => {
|
|
764
|
+
return async (notificationUserDocument) => {
|
|
765
|
+
// run updates in batches
|
|
766
|
+
let notificationBoxesUpdated = 0;
|
|
767
|
+
let hasMoreNotificationBoxesToSync = true;
|
|
768
|
+
while (hasMoreNotificationBoxesToSync) {
|
|
769
|
+
const batchResult = await firestoreContext.runTransaction(async (transaction) => {
|
|
770
|
+
const notificationUserDocumentInTransaction = notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationUserDocument);
|
|
771
|
+
const notificationUser = await firebaseServer.assertSnapshotData(notificationUserDocumentInTransaction);
|
|
772
|
+
const { gc } = notificationUser;
|
|
773
|
+
const notificationBoxConfigsToSync = notificationUser.bc.filter((x) => x.ns);
|
|
774
|
+
const notificationBoxConfigsToSyncInThisBatch = util.takeFront(notificationBoxConfigsToSync, MAX_NOTIFICATION_BOXES_TO_UPDATE_PER_BATCH);
|
|
775
|
+
/**
|
|
776
|
+
* These are the actual number of NotificationBox values that had recipients updated.
|
|
777
|
+
*/
|
|
778
|
+
let notificationBoxesUpdatedInBatch = 0;
|
|
779
|
+
let hasUnsyncedNotificationBoxConfigs = false;
|
|
780
|
+
if (notificationBoxConfigsToSyncInThisBatch.length > 0) {
|
|
781
|
+
const notificationBoxConfigsToSyncInThisBatchMap = util.makeModelMap(notificationBoxConfigsToSyncInThisBatch, (x) => x.nb);
|
|
782
|
+
const notificationBoxIdsToSyncInThisBatch = Array.from(notificationBoxConfigsToSyncInThisBatchMap.keys());
|
|
783
|
+
const notificationBoxDocuments = firebase.loadDocumentsForIds(notificationBoxCollection.documentAccessorForTransaction(transaction), notificationBoxIdsToSyncInThisBatch);
|
|
784
|
+
const notificationBoxDocumentSnapshotDataPairs = await firebase.getDocumentSnapshotDataPairs(notificationBoxDocuments);
|
|
785
|
+
const notificationBoxConfigsToRemoveFromNotificationUser = new Set();
|
|
786
|
+
const notificationUserNotificationBoxConfigsToMarkAsRemoved = new Set();
|
|
787
|
+
const nextRecipientsMap = new Map();
|
|
788
|
+
// update each NotificationBoxDocument
|
|
789
|
+
await util.performAsyncTasks(notificationBoxDocumentSnapshotDataPairs, async (notificationBoxDocumentSnapshotDataPair) => {
|
|
790
|
+
const { data: notificationBox, document } = notificationBoxDocumentSnapshotDataPair;
|
|
791
|
+
const nb = document.id;
|
|
792
|
+
const notificationUserNotificationBoxConfig = notificationBoxConfigsToSyncInThisBatchMap.get(nb); // always exists
|
|
793
|
+
if (!notificationBox) {
|
|
794
|
+
// if the entire NotificationBox no longer exists, flag to remove it from the user as a cleanup measure
|
|
795
|
+
notificationBoxConfigsToRemoveFromNotificationUser.add(nb);
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
// update in the NotificationBox
|
|
799
|
+
const recipientIndex = notificationBox.r.findIndex((x) => x.uid === notificationUser.uid);
|
|
800
|
+
let r;
|
|
801
|
+
if (recipientIndex === -1) {
|
|
802
|
+
// if they are not in the NotificationBox, then mark them as removed on the user
|
|
803
|
+
notificationUserNotificationBoxConfigsToMarkAsRemoved.add(nb);
|
|
804
|
+
}
|
|
805
|
+
else if (notificationUserNotificationBoxConfig.rm) {
|
|
806
|
+
// remove from the notification box if it is flagged
|
|
807
|
+
r = util.removeValuesAtIndexesFromArrayCopy(notificationBox.r, recipientIndex);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
const { m } = notificationBox;
|
|
811
|
+
const recipient = notificationBox.r[recipientIndex];
|
|
812
|
+
const nextRecipient = firebase.effectiveNotificationBoxRecipientConfig({
|
|
813
|
+
uid: notificationUser.uid,
|
|
814
|
+
m,
|
|
815
|
+
appNotificationTemplateTypeInfoRecordService,
|
|
816
|
+
gc,
|
|
817
|
+
boxConfig: notificationUserNotificationBoxConfig,
|
|
818
|
+
recipient
|
|
819
|
+
});
|
|
820
|
+
const recipientHasChange = !util.areEqualPOJOValues(nextRecipient, recipient);
|
|
821
|
+
// only update recipients if the next/new recipient is not equal to the existing one
|
|
822
|
+
if (recipientHasChange) {
|
|
823
|
+
r = [...notificationBox.r];
|
|
824
|
+
r[recipientIndex] = nextRecipient;
|
|
825
|
+
nextRecipientsMap.set(nb, nextRecipient);
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
nextRecipientsMap.set(nb, recipient);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// update recipients if needed
|
|
832
|
+
if (r != null) {
|
|
833
|
+
await document.update({ r });
|
|
834
|
+
notificationBoxesUpdatedInBatch += 1;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
// Update the NotificationUser
|
|
839
|
+
const notificationBoxIdsSynced = new Set(notificationBoxIdsToSyncInThisBatch);
|
|
840
|
+
// start nextConfigs off as a new array with none of the sync'd ids
|
|
841
|
+
const nextConfigs = notificationBoxConfigsToSyncInThisBatch.filter((x) => !notificationBoxIdsSynced.has(x.nb));
|
|
842
|
+
notificationBoxIdsToSyncInThisBatch.forEach((nb) => {
|
|
843
|
+
let nextConfig;
|
|
844
|
+
if (notificationBoxConfigsToRemoveFromNotificationUser.has(nb)) ;
|
|
845
|
+
else {
|
|
846
|
+
const existingConfig = notificationBoxConfigsToSyncInThisBatchMap.get(nb);
|
|
847
|
+
if (notificationUserNotificationBoxConfigsToMarkAsRemoved.has(nb) || existingConfig.rm) {
|
|
848
|
+
// if the recipient was being removed or is marked as removed, then update the config to confirm removal
|
|
849
|
+
nextConfig = {
|
|
850
|
+
...existingConfig,
|
|
851
|
+
nb,
|
|
852
|
+
rm: true,
|
|
853
|
+
i: util.UNSET_INDEX_NUMBER
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
// else, use the updated recipient and keep/copy the
|
|
858
|
+
const updatedRecipient = nextRecipientsMap.get(nb);
|
|
859
|
+
nextConfig = {
|
|
860
|
+
...existingConfig,
|
|
861
|
+
nb,
|
|
862
|
+
rm: false, // mark as not removed
|
|
863
|
+
i: updatedRecipient.i ?? util.UNSET_INDEX_NUMBER
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (nextConfig != null) {
|
|
868
|
+
nextConfig.ns = false; // mark as synced
|
|
869
|
+
nextConfigs.push(nextConfig);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
const ns = nextConfigs.some((x) => x.ns);
|
|
873
|
+
await notificationUserDocumentInTransaction.update({ bc: nextConfigs, ns });
|
|
874
|
+
hasUnsyncedNotificationBoxConfigs = ns;
|
|
875
|
+
}
|
|
876
|
+
const batchResult = {
|
|
877
|
+
hasMoreNotificationBoxesToSync: hasUnsyncedNotificationBoxConfigs,
|
|
878
|
+
notificationBoxesUpdatedInBatch
|
|
879
|
+
};
|
|
880
|
+
return batchResult;
|
|
881
|
+
});
|
|
882
|
+
hasMoreNotificationBoxesToSync = batchResult.hasMoreNotificationBoxesToSync;
|
|
883
|
+
notificationBoxesUpdated += batchResult.notificationBoxesUpdatedInBatch;
|
|
884
|
+
}
|
|
885
|
+
const result = {
|
|
886
|
+
notificationBoxesUpdated
|
|
887
|
+
};
|
|
888
|
+
return result;
|
|
889
|
+
};
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
function resyncAllNotificationUsersFactory(context) {
|
|
893
|
+
const { notificationUserCollection } = context;
|
|
894
|
+
const resyncNotificationUser = resyncNotificationUserFactory(context);
|
|
895
|
+
return async () => {
|
|
896
|
+
let notificationBoxesUpdated = 0;
|
|
897
|
+
const resyncNotificationUserParams = { key: firebase.firestoreDummyKey() };
|
|
898
|
+
const resyncNotificationUserInstance = await resyncNotificationUser(resyncNotificationUserParams);
|
|
899
|
+
const iterateResult = await firebase.iterateFirestoreDocumentSnapshotPairs({
|
|
900
|
+
documentAccessor: notificationUserCollection.documentAccessor(),
|
|
901
|
+
iterateSnapshotPair: async (snapshotPair) => {
|
|
902
|
+
const { document: notificationUserDocument } = snapshotPair;
|
|
903
|
+
const result = await resyncNotificationUserInstance(notificationUserDocument);
|
|
904
|
+
notificationBoxesUpdated += result.notificationBoxesUpdated;
|
|
905
|
+
},
|
|
906
|
+
constraintsFactory: () => firebase.notificationUsersFlaggedForNeedsSyncQuery(),
|
|
907
|
+
snapshotsPerformTasksConfig: {
|
|
908
|
+
// prevent NotificationUsers with the same NotificationBoxes from being updated/sync'd at the same time
|
|
909
|
+
nonConcurrentTaskKeyFactory: (x) => {
|
|
910
|
+
const notificationBoxIdsToSync = x
|
|
911
|
+
.data()
|
|
912
|
+
.bc.filter((x) => x.ns)
|
|
913
|
+
.map((x) => x.nb);
|
|
914
|
+
return notificationBoxIdsToSync;
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
queryFactory: notificationUserCollection,
|
|
918
|
+
batchSize: undefined,
|
|
919
|
+
performTasksConfig: {
|
|
920
|
+
maxParallelTasks: 10
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
const result = {
|
|
924
|
+
notificationUsersResynced: iterateResult.totalSnapshotsVisited,
|
|
925
|
+
notificationBoxesUpdated
|
|
926
|
+
};
|
|
927
|
+
return result;
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function createNotificationSummaryFactory(context) {
|
|
931
|
+
const { firebaseServerActionTransformFunctionFactory, notificationSummaryCollection } = context;
|
|
932
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CreateNotificationSummaryParams, async (params) => {
|
|
933
|
+
const { model } = params;
|
|
934
|
+
return async () => {
|
|
935
|
+
const notificationSummaryId = firebase.notificationSummaryIdForModel(model);
|
|
936
|
+
const notificationSummaryDocument = notificationSummaryCollection.documentAccessor().loadDocumentForId(notificationSummaryId);
|
|
937
|
+
const newSummaryTemplate = makeNewNotificationSummaryTemplate(model);
|
|
938
|
+
await notificationSummaryDocument.create(newSummaryTemplate);
|
|
939
|
+
return notificationSummaryDocument;
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
function updateNotificationSummaryFactory(context) {
|
|
944
|
+
const { firebaseServerActionTransformFunctionFactory } = context;
|
|
945
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateNotificationSummaryParams, async (params) => {
|
|
946
|
+
const { setReadAtTime, flagAllRead } = params;
|
|
947
|
+
return async (notificationSummaryDocument) => {
|
|
948
|
+
let updateTemplate;
|
|
949
|
+
if (setReadAtTime != null) {
|
|
950
|
+
updateTemplate = { rat: setReadAtTime };
|
|
951
|
+
}
|
|
952
|
+
else if (flagAllRead === true) {
|
|
953
|
+
updateTemplate = { rat: new Date() };
|
|
954
|
+
}
|
|
955
|
+
if (updateTemplate != null) {
|
|
956
|
+
await notificationSummaryDocument.update(updateTemplate);
|
|
957
|
+
}
|
|
958
|
+
return notificationSummaryDocument;
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function createNotificationBoxInTransactionFactory(context) {
|
|
963
|
+
const { notificationBoxCollection } = context;
|
|
964
|
+
return async (params, transaction) => {
|
|
965
|
+
const { now: inputNow, skipCreate } = params;
|
|
966
|
+
const now = inputNow ?? new Date();
|
|
967
|
+
const notificationBoxDocument = firebase.loadNotificationBoxDocumentForReferencePair(params, notificationBoxCollection.documentAccessorForTransaction(transaction));
|
|
968
|
+
const notificationBoxTemplate = {
|
|
969
|
+
m: notificationBoxDocument.notificationBoxRelatedModelKey,
|
|
970
|
+
o: firebase.firestoreDummyKey(), // set during initialization
|
|
971
|
+
r: [],
|
|
972
|
+
cat: now,
|
|
973
|
+
w: date.yearWeekCode(now),
|
|
974
|
+
s: true // requires initialization
|
|
975
|
+
};
|
|
976
|
+
if (!skipCreate) {
|
|
977
|
+
await notificationBoxDocument.create(notificationBoxTemplate);
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
notificationBoxTemplate,
|
|
981
|
+
notificationBoxDocument
|
|
982
|
+
};
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function createNotificationBoxFactory(context) {
|
|
986
|
+
const { firestoreContext, notificationBoxCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
987
|
+
const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
|
|
988
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CreateNotificationBoxParams, async (params) => {
|
|
989
|
+
const { model } = params;
|
|
990
|
+
return async () => {
|
|
991
|
+
const result = await firestoreContext.runTransaction(async (transaction) => {
|
|
992
|
+
const { notificationBoxDocument } = await createNotificationBoxInTransaction({ notificationBoxRelatedModelKey: model }, transaction);
|
|
993
|
+
return notificationBoxDocument;
|
|
994
|
+
});
|
|
995
|
+
return notificationBoxCollection.documentAccessor().loadDocumentFrom(result);
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
function updateNotificationBoxFactory({ firebaseServerActionTransformFunctionFactory }) {
|
|
1000
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateNotificationBoxParams, async () => {
|
|
1001
|
+
return async (notificationBoxDocument) => {
|
|
1002
|
+
// does nothing currently.
|
|
1003
|
+
return notificationBoxDocument;
|
|
1004
|
+
};
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function updateNotificationBoxRecipientExclusionInTransactionFactory(context) {
|
|
1008
|
+
const { notificationBoxCollection, notificationUserCollection } = context;
|
|
1009
|
+
return async (input, transaction) => {
|
|
1010
|
+
const { params } = input;
|
|
1011
|
+
const { uid: inputUid, i, setExclusion } = params;
|
|
1012
|
+
const notificationBoxDocument = firebase.loadNotificationBoxDocumentForReferencePair(input, notificationBoxCollection.documentAccessorForTransaction(transaction));
|
|
1013
|
+
let targetUid = inputUid;
|
|
1014
|
+
let result = undefined;
|
|
1015
|
+
if (setExclusion == null) {
|
|
1016
|
+
throw new Error('setExclusion was undefined. Maybe you wanted to call updateNotificationBoxRecipientInTransactionFactory() instead?');
|
|
1017
|
+
}
|
|
1018
|
+
else if (!inputUid && i != null) {
|
|
1019
|
+
// only load the notification box if targeting a recipient by index
|
|
1020
|
+
const notificationBox = await notificationBoxDocument.snapshotData();
|
|
1021
|
+
if (!notificationBox) {
|
|
1022
|
+
throw notificationBoxExclusionTargetInvalidError();
|
|
1023
|
+
}
|
|
1024
|
+
const targetRecipient = notificationBox.r.find((x) => x.i === i);
|
|
1025
|
+
if (!targetRecipient || !targetRecipient.uid) {
|
|
1026
|
+
throw notificationBoxExclusionTargetInvalidError();
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
targetUid = targetRecipient.uid;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (!targetUid) {
|
|
1033
|
+
throw notificationBoxExclusionTargetInvalidError();
|
|
1034
|
+
}
|
|
1035
|
+
const notificationUserDocument = await notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentForId(targetUid);
|
|
1036
|
+
const notificationUser = await notificationUserDocument.snapshotData();
|
|
1037
|
+
if (notificationUser) {
|
|
1038
|
+
// only update if the user exists
|
|
1039
|
+
const targetExclusions = [notificationBoxDocument.id];
|
|
1040
|
+
const { update: notificationUserUpdate } = firebase.updateNotificationUserNotificationSendExclusions({
|
|
1041
|
+
notificationUser,
|
|
1042
|
+
addExclusions: setExclusion ? targetExclusions : undefined,
|
|
1043
|
+
removeExclusions: setExclusion ? undefined : targetExclusions
|
|
1044
|
+
});
|
|
1045
|
+
await notificationUserDocument.update(notificationUserUpdate);
|
|
1046
|
+
result = {
|
|
1047
|
+
notificationUserUpdate
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function updateNotificationBoxRecipientInTransactionFactory(context) {
|
|
1054
|
+
const { authService, notificationBoxCollection, notificationUserCollection } = context;
|
|
1055
|
+
const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
|
|
1056
|
+
return async (input, transaction) => {
|
|
1057
|
+
const { params, allowCreateNotificationBoxIfItDoesNotExist, throwErrorIfNotificationBoxDoesNotExist } = input;
|
|
1058
|
+
const { uid, i, insert, remove, configs: inputC, setExclusion } = params;
|
|
1059
|
+
const findRecipientFn = (x) => (uid != null && x.uid === uid) || (i != null && x.i === i);
|
|
1060
|
+
if (setExclusion != null) {
|
|
1061
|
+
throw new Error('exclusion update must be processed by updateNotificationBoxRecipientExclusionInTransactionFactory() function.');
|
|
1062
|
+
}
|
|
1063
|
+
const notificationBoxDocument = firebase.loadNotificationBoxDocumentForReferencePair(input, notificationBoxCollection.documentAccessorForTransaction(transaction));
|
|
1064
|
+
let notificationBox = await notificationBoxDocument.snapshotData();
|
|
1065
|
+
let createNotificationBox = false;
|
|
1066
|
+
let result = undefined;
|
|
1067
|
+
if (!notificationBox) {
|
|
1068
|
+
if (allowCreateNotificationBoxIfItDoesNotExist) {
|
|
1069
|
+
const { notificationBoxTemplate } = await createNotificationBoxInTransaction({
|
|
1070
|
+
notificationBoxDocument,
|
|
1071
|
+
skipCreate: true // don't create since we still need to read things for the transaction
|
|
1072
|
+
}, transaction);
|
|
1073
|
+
notificationBox = notificationBoxTemplate;
|
|
1074
|
+
createNotificationBox = true;
|
|
1075
|
+
}
|
|
1076
|
+
else if (throwErrorIfNotificationBoxDoesNotExist) {
|
|
1077
|
+
throw notificationBoxDoesNotExist();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (notificationBox) {
|
|
1081
|
+
const { m } = notificationBox;
|
|
1082
|
+
let r;
|
|
1083
|
+
let targetRecipientIndex = notificationBox.r.findIndex(findRecipientFn);
|
|
1084
|
+
const targetRecipient = notificationBox.r[targetRecipientIndex];
|
|
1085
|
+
let nextRecipient;
|
|
1086
|
+
if (remove) {
|
|
1087
|
+
if (targetRecipientIndex != null) {
|
|
1088
|
+
r = [...notificationBox.r]; // remove if they exist.
|
|
1089
|
+
delete r[targetRecipientIndex];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
if (!targetRecipient && !insert) {
|
|
1094
|
+
throw notificationBoxRecipientDoesNotExistsError();
|
|
1095
|
+
}
|
|
1096
|
+
const c = (inputC != null ? firebase.notificationBoxRecipientTemplateConfigArrayToRecord(inputC) : targetRecipient?.c) ?? {};
|
|
1097
|
+
nextRecipient = {
|
|
1098
|
+
uid,
|
|
1099
|
+
i: targetRecipient?.i ?? util.UNSET_INDEX_NUMBER,
|
|
1100
|
+
c,
|
|
1101
|
+
...firebase.updateNotificationRecipient(targetRecipient ?? {}, params)
|
|
1102
|
+
};
|
|
1103
|
+
r = [...notificationBox.r];
|
|
1104
|
+
if (targetRecipient) {
|
|
1105
|
+
nextRecipient.i = targetRecipient.i;
|
|
1106
|
+
nextRecipient = firebase.mergeNotificationBoxRecipients(targetRecipient, nextRecipient);
|
|
1107
|
+
r[targetRecipientIndex] = nextRecipient; // override in the array
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
const nextI = util.computeNextFreeIndexOnSortedValuesFunction(util.readIndexNumber)(notificationBox.r); // r is sorted by index in ascending order, so the last value is the largest i
|
|
1111
|
+
nextRecipient.i = nextI;
|
|
1112
|
+
// should have the greatest i value, push to end
|
|
1113
|
+
r.push(nextRecipient);
|
|
1114
|
+
targetRecipientIndex = r.length - 1;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
// save changes to r if it has changed
|
|
1118
|
+
if (r != null) {
|
|
1119
|
+
const notificationUserId = targetRecipient?.uid ?? nextRecipient?.uid;
|
|
1120
|
+
// sync with the notification user's document, if it exists
|
|
1121
|
+
if (notificationUserId != null) {
|
|
1122
|
+
const notificationBoxId = notificationBoxDocument.id;
|
|
1123
|
+
const notificationUserDocument = await notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentForId(notificationUserId);
|
|
1124
|
+
let notificationUser = await notificationUserDocument.snapshotData();
|
|
1125
|
+
const createNotificationUser = !notificationUser && !remove && insert;
|
|
1126
|
+
if (createNotificationUser) {
|
|
1127
|
+
// assert they exist in the auth system
|
|
1128
|
+
const userContext = authService.userContext(notificationUserId);
|
|
1129
|
+
const userExistsInAuth = await userContext.exists();
|
|
1130
|
+
if (!userExistsInAuth) {
|
|
1131
|
+
throw notificationUserInvalidUidForCreateError(notificationUserId);
|
|
1132
|
+
}
|
|
1133
|
+
const notificationUserTemplate = {
|
|
1134
|
+
uid: notificationUserId,
|
|
1135
|
+
b: [],
|
|
1136
|
+
x: [],
|
|
1137
|
+
bc: [],
|
|
1138
|
+
ns: false,
|
|
1139
|
+
dc: {
|
|
1140
|
+
c: {}
|
|
1141
|
+
},
|
|
1142
|
+
gc: {
|
|
1143
|
+
c: {}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
notificationUser = notificationUserTemplate;
|
|
1147
|
+
}
|
|
1148
|
+
// if the user is being inserted or exists, then make updates
|
|
1149
|
+
if (notificationUser != null) {
|
|
1150
|
+
const { updatedBc, updatedNotificationBoxRecipient } = updateNotificationUserNotificationBoxRecipientConfig({
|
|
1151
|
+
notificationBoxId,
|
|
1152
|
+
notificationUserId,
|
|
1153
|
+
notificationUser,
|
|
1154
|
+
insertingRecipientIntoNotificationBox: insert,
|
|
1155
|
+
removeRecipientFromNotificationBox: remove,
|
|
1156
|
+
notificationBoxRecipient: nextRecipient
|
|
1157
|
+
});
|
|
1158
|
+
const updatedB = updatedBc ? updatedBc.map((x) => x.nb) : undefined;
|
|
1159
|
+
if (createNotificationUser) {
|
|
1160
|
+
const newUserTemplate = {
|
|
1161
|
+
...notificationUser,
|
|
1162
|
+
bc: updatedBc ?? [],
|
|
1163
|
+
b: updatedB ?? []
|
|
1164
|
+
};
|
|
1165
|
+
await notificationUserDocument.create(newUserTemplate);
|
|
1166
|
+
}
|
|
1167
|
+
else if (updatedBc != null) {
|
|
1168
|
+
await notificationUserDocument.update({ bc: updatedBc, b: updatedB });
|
|
1169
|
+
}
|
|
1170
|
+
// Set if nextRecipient is updated/influence from existing configuration
|
|
1171
|
+
if (targetRecipientIndex != null && updatedNotificationBoxRecipient && !remove) {
|
|
1172
|
+
r[targetRecipientIndex] = updatedNotificationBoxRecipient; // set the updated value in r
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// else, if removing and they don't exist, nothing to update
|
|
1176
|
+
}
|
|
1177
|
+
const updatedNotificationBox = { ...notificationBox, r };
|
|
1178
|
+
let notificationBoxWasCreated = false;
|
|
1179
|
+
if (createNotificationBox) {
|
|
1180
|
+
await notificationBoxDocument.create(updatedNotificationBox);
|
|
1181
|
+
notificationBoxWasCreated = true;
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
await notificationBoxDocument.update({ r });
|
|
1185
|
+
}
|
|
1186
|
+
result = {
|
|
1187
|
+
updatedNotificationBox,
|
|
1188
|
+
notificationBoxWasCreated,
|
|
1189
|
+
notificationBoxDocument
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return result;
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function updateNotificationBoxRecipientFactory(context) {
|
|
1197
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
1198
|
+
const updateNotificationBoxRecipientInTransaction = updateNotificationBoxRecipientInTransactionFactory(context);
|
|
1199
|
+
const updateNotificationBoxRecipientExclusionInTransaction = updateNotificationBoxRecipientExclusionInTransactionFactory(context);
|
|
1200
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateNotificationBoxRecipientParams, async (params) => {
|
|
1201
|
+
return async (notificationBoxDocument) => {
|
|
1202
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
1203
|
+
if (params.setExclusion != null) {
|
|
1204
|
+
await updateNotificationBoxRecipientExclusionInTransaction({
|
|
1205
|
+
params,
|
|
1206
|
+
notificationBoxDocument
|
|
1207
|
+
}, transaction);
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
await updateNotificationBoxRecipientInTransaction({
|
|
1211
|
+
params,
|
|
1212
|
+
throwErrorIfNotificationBoxDoesNotExist: true,
|
|
1213
|
+
notificationBoxDocument
|
|
1214
|
+
}, transaction);
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
return notificationBoxDocument;
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = 8;
|
|
1222
|
+
const UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 1;
|
|
1223
|
+
const UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY = 8;
|
|
1224
|
+
const UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 1;
|
|
1225
|
+
const KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
|
|
1226
|
+
const KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 5;
|
|
1227
|
+
const NOTIFICATION_MAX_SEND_ATTEMPTS = 5;
|
|
1228
|
+
const NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES = 8;
|
|
1229
|
+
/**
|
|
1230
|
+
* Minimum time in minutes that a notification task can be attempted again
|
|
1231
|
+
*/
|
|
1232
|
+
const NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES = 1;
|
|
1233
|
+
const NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS = 5;
|
|
1234
|
+
const NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS = 3;
|
|
1235
|
+
const NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS = dateFns.hoursToMilliseconds(NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS);
|
|
1236
|
+
function sendNotificationFactory(context) {
|
|
1237
|
+
const { appNotificationTemplateTypeInfoRecordService, notificationSendService, notificationTaskService, notificationTemplateService, authService, notificationBoxCollection, notificationCollectionGroup, notificationUserCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
1238
|
+
const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
|
|
1239
|
+
const notificationUserAccessor = notificationUserCollection.documentAccessor();
|
|
1240
|
+
return firebaseServerActionTransformFunctionFactory(firebase.SendNotificationParams, async (params) => {
|
|
1241
|
+
const { ignoreSendAtThrottle } = params;
|
|
1242
|
+
return async (inputNotificationDocument) => {
|
|
1243
|
+
const now = new Date();
|
|
1244
|
+
// Load the notification document outside of any potential context (transaction, etc.)
|
|
1245
|
+
const notificationDocument = notificationCollectionGroup.documentAccessor().loadDocumentFrom(inputNotificationDocument);
|
|
1246
|
+
const { nextSat, throttled, tryRun, isNotificationTask, notificationTaskHandler, notification, createdBox, notificationBoxNeedsInitialization, notificationBox, notificationBoxModelKey, deletedNotification, templateInstance, isConfiguredTemplateType, isKnownTemplateType, onlySendToExplicitlyEnabledRecipients, onlyTextExplicitlyEnabledRecipients } = await firestoreContext.runTransaction(async (transaction) => {
|
|
1247
|
+
const notificationBoxDocument = notificationBoxCollection.documentAccessorForTransaction(transaction).loadDocument(notificationDocument.parent);
|
|
1248
|
+
const notificationDocumentInTransaction = notificationCollectionGroup.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationDocument);
|
|
1249
|
+
let [notificationBox, notification] = await Promise.all([firebase.getDocumentSnapshotData(notificationBoxDocument), firebase.getDocumentSnapshotData(notificationDocumentInTransaction)]);
|
|
1250
|
+
const model = firebase.inferKeyFromTwoWayFlatFirestoreModelKey(notificationBoxDocument.id);
|
|
1251
|
+
const isNotificationTask = notification?.st === firebase.NotificationSendType.TASK_NOTIFICATION;
|
|
1252
|
+
let tryRun = true;
|
|
1253
|
+
let throttled = false;
|
|
1254
|
+
let nextSat;
|
|
1255
|
+
if (!notification) {
|
|
1256
|
+
tryRun = false;
|
|
1257
|
+
}
|
|
1258
|
+
else if (!ignoreSendAtThrottle) {
|
|
1259
|
+
tryRun = !dateFns.isFuture(notification.sat);
|
|
1260
|
+
if (!tryRun) {
|
|
1261
|
+
throttled = true;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// always set nextSat if tryRun is true
|
|
1265
|
+
if (tryRun) {
|
|
1266
|
+
if (isNotificationTask) {
|
|
1267
|
+
// can try to run the task again in 1 minute
|
|
1268
|
+
nextSat = dateFns.addMinutes(now, NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES);
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
// update the next send type of non-tasks to try being sent again in 10 minutes, if they fail
|
|
1272
|
+
nextSat = dateFns.addMinutes(now, 10);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
let createdBox = false;
|
|
1276
|
+
let deletedNotification = false;
|
|
1277
|
+
let notificationBoxNeedsInitialization = false;
|
|
1278
|
+
let isKnownTemplateType;
|
|
1279
|
+
let isConfiguredTemplateType;
|
|
1280
|
+
let onlySendToExplicitlyEnabledRecipients;
|
|
1281
|
+
let onlyTextExplicitlyEnabledRecipients;
|
|
1282
|
+
let templateInstance;
|
|
1283
|
+
let notificationTaskHandler;
|
|
1284
|
+
async function deleteNotification() {
|
|
1285
|
+
tryRun = false;
|
|
1286
|
+
await notificationDocumentInTransaction.accessor.delete();
|
|
1287
|
+
deletedNotification = true;
|
|
1288
|
+
}
|
|
1289
|
+
// create/init the notification box if necessary/configured.
|
|
1290
|
+
if (notification && tryRun) {
|
|
1291
|
+
// if we're still trying to run, check the template is ok. If not, cancel the run.
|
|
1292
|
+
const { t // notification task/template type
|
|
1293
|
+
} = notification.n;
|
|
1294
|
+
if (isNotificationTask) {
|
|
1295
|
+
notificationTaskHandler = notificationTaskService.taskHandlerForNotificationTaskType(t);
|
|
1296
|
+
if (notificationTaskHandler) {
|
|
1297
|
+
if (notification.a >= NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS) {
|
|
1298
|
+
tryRun = false;
|
|
1299
|
+
console.warn(`Configured notification task of type "${t}" has reached the delete threshhold after being attempted ${notification.a} times. Deleting notification task.`);
|
|
1300
|
+
await deleteNotification();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
tryRun = false;
|
|
1305
|
+
const delay = UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY;
|
|
1306
|
+
if (notification.a < UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS) {
|
|
1307
|
+
console.warn(`Notification task type of "${t}" was found in a Notification but has no handler. Action is being delayed by ${delay} hours.`);
|
|
1308
|
+
nextSat = dateFns.addHours(now, delay);
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
console.warn(`Notification task type of "${t}" was found in a Notification but has no handler. Action is being deleted.`);
|
|
1312
|
+
// delete the notification
|
|
1313
|
+
await deleteNotification();
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
templateInstance = notificationTemplateService.templateInstanceForType(t);
|
|
1319
|
+
isConfiguredTemplateType = templateInstance.isConfiguredType;
|
|
1320
|
+
const templateTypeInfo = appNotificationTemplateTypeInfoRecordService.appNotificationTemplateTypeInfoRecord[t];
|
|
1321
|
+
isKnownTemplateType = templateTypeInfo != null;
|
|
1322
|
+
onlySendToExplicitlyEnabledRecipients = notification.ois ?? templateTypeInfo?.onlySendToExplicitlyEnabledRecipients;
|
|
1323
|
+
onlyTextExplicitlyEnabledRecipients = notification.ots ?? templateTypeInfo?.onlyTextExplicitlyEnabledRecipients;
|
|
1324
|
+
if (!isConfiguredTemplateType) {
|
|
1325
|
+
// log the issue that an notification with an unconfigured type was queued
|
|
1326
|
+
const retryAttempts = isKnownTemplateType ? KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS : UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS;
|
|
1327
|
+
const delay = isKnownTemplateType ? KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY : UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
|
|
1328
|
+
if (notification.a < retryAttempts) {
|
|
1329
|
+
if (isKnownTemplateType) {
|
|
1330
|
+
console.warn(`Unconfigured but known template type of "${t}" (${templateTypeInfo.name}) was found in a Notification. Send is being delayed by ${delay} hours.`);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
console.warn(`Unknown template type of "${t}" was found in a Notification. Send is being delayed by ${delay} hours.`);
|
|
1334
|
+
}
|
|
1335
|
+
// delay send for 12 hours, for a max of 24 hours incase it is an issue.
|
|
1336
|
+
nextSat = dateFns.addHours(now, delay);
|
|
1337
|
+
tryRun = false;
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
console.warn(`Unconfigured template type of "${t}" was found in a Notification. The Notification has reached the delete threshhold after failing to send due to misconfiguration multiple times and is being deleted.`);
|
|
1341
|
+
// after attempting to send 3 times, delete it.
|
|
1342
|
+
await deleteNotification();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
// handle the notification box's absence
|
|
1346
|
+
if (!notificationBox && tryRun) {
|
|
1347
|
+
switch (notification.st) {
|
|
1348
|
+
case firebase.NotificationSendType.INIT_BOX_AND_SEND:
|
|
1349
|
+
const { notificationBoxTemplate } = await createNotificationBoxInTransaction({ notificationBoxDocument }, transaction);
|
|
1350
|
+
notificationBox = firebase.setIdAndKeyFromKeyIdRefOnDocumentData(notificationBoxTemplate, notificationBoxDocument);
|
|
1351
|
+
createdBox = true;
|
|
1352
|
+
break;
|
|
1353
|
+
case firebase.NotificationSendType.SEND_IF_BOX_EXISTS:
|
|
1354
|
+
// delete the notification since it won't get sent.
|
|
1355
|
+
await deleteNotification();
|
|
1356
|
+
break;
|
|
1357
|
+
case firebase.NotificationSendType.SEND_WITHOUT_CREATING_BOX:
|
|
1358
|
+
// continue with current tryRun
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
// if the notification box is not initialized/synchronized yet, do not run.
|
|
1363
|
+
if (tryRun && notificationBox && notificationBox.s) {
|
|
1364
|
+
notificationBoxNeedsInitialization = true;
|
|
1365
|
+
tryRun = false;
|
|
1366
|
+
nextSat = dateFns.addMinutes(now, NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
// update the notification send at time and attempt count
|
|
1371
|
+
if (notification != null && nextSat != null && !deletedNotification) {
|
|
1372
|
+
const isAtMaxAttempts = notification.a >= NOTIFICATION_MAX_SEND_ATTEMPTS;
|
|
1373
|
+
if (isAtMaxAttempts && notificationBoxNeedsInitialization) {
|
|
1374
|
+
await deleteNotification(); // just delete the notification if the box still hasn't been initialized successfully at this point.
|
|
1375
|
+
}
|
|
1376
|
+
// check if it was just deleted
|
|
1377
|
+
if (!deletedNotification) {
|
|
1378
|
+
const a = isNotificationTask && tryRun ? notification.a : notification.a + 1; // do not update a notification task's attempt count here, unless tryRun fails
|
|
1379
|
+
// NOTE: It is important to update sat so the notification task queue running doesn't get stuck in a query loop by notifications/tasks that have a sat value that is in the past, but was just run.
|
|
1380
|
+
await notificationDocumentInTransaction.update({ sat: nextSat, a });
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
nextSat,
|
|
1385
|
+
throttled,
|
|
1386
|
+
isNotificationTask,
|
|
1387
|
+
deletedNotification,
|
|
1388
|
+
createdBox,
|
|
1389
|
+
notificationBoxModelKey: model,
|
|
1390
|
+
notificationBoxNeedsInitialization,
|
|
1391
|
+
notificationBox,
|
|
1392
|
+
notification,
|
|
1393
|
+
templateInstance,
|
|
1394
|
+
isKnownTemplateType,
|
|
1395
|
+
notificationTaskHandler,
|
|
1396
|
+
isConfiguredTemplateType,
|
|
1397
|
+
tryRun,
|
|
1398
|
+
onlySendToExplicitlyEnabledRecipients,
|
|
1399
|
+
onlyTextExplicitlyEnabledRecipients
|
|
1400
|
+
};
|
|
1401
|
+
});
|
|
1402
|
+
let success = false;
|
|
1403
|
+
let isUniqueNotificationTask = false;
|
|
1404
|
+
let uniqueNotificationTaskConflict = false;
|
|
1405
|
+
let sendEmailsResult;
|
|
1406
|
+
let sendTextsResult;
|
|
1407
|
+
let sendNotificationSummaryResult;
|
|
1408
|
+
let loadMessageFunctionFailure = false;
|
|
1409
|
+
let buildMessageFailure = false;
|
|
1410
|
+
let notificationMarkedDone = false;
|
|
1411
|
+
let notificationTaskCompletionType;
|
|
1412
|
+
let notificationTaskPartsRunCount = 0;
|
|
1413
|
+
let notificationTaskLoopingProtectionTriggered;
|
|
1414
|
+
let onSendAttemptedResult;
|
|
1415
|
+
let onSendSuccessResult;
|
|
1416
|
+
const notificationTemplateType = templateInstance?.type;
|
|
1417
|
+
if (isNotificationTask) {
|
|
1418
|
+
await handleNotificationTask();
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
await handleNormalNotification();
|
|
1422
|
+
}
|
|
1423
|
+
async function _runNotificationTaskNextPart(input) {
|
|
1424
|
+
const { notification, notificationTaskHandler, previouslyCompleteSubTasks } = input;
|
|
1425
|
+
const { n: item, cat, ut } = notification;
|
|
1426
|
+
let tryRunNextPart = false;
|
|
1427
|
+
let partNotificationTaskCompletionType;
|
|
1428
|
+
let partNotificationMarkedDone = false;
|
|
1429
|
+
let partTprReversal = false;
|
|
1430
|
+
let partSuccess = false;
|
|
1431
|
+
let nextCompleteSubTasks;
|
|
1432
|
+
const unique = ut ?? false;
|
|
1433
|
+
const notificationTask = {
|
|
1434
|
+
notificationDocument,
|
|
1435
|
+
totalSendAttempts: notification.a,
|
|
1436
|
+
currentCheckpointSendAttempts: notification.at ?? 0,
|
|
1437
|
+
taskType: item.t,
|
|
1438
|
+
item,
|
|
1439
|
+
data: item.d,
|
|
1440
|
+
checkpoints: notification.tpr,
|
|
1441
|
+
createdAt: cat,
|
|
1442
|
+
unique
|
|
1443
|
+
};
|
|
1444
|
+
// calculate results
|
|
1445
|
+
const notificationTemplate = {};
|
|
1446
|
+
// perform the task
|
|
1447
|
+
try {
|
|
1448
|
+
const handleTaskResult = await notificationTaskHandler.handleNotificationTask(notificationTask);
|
|
1449
|
+
const { completion, updateMetadata, delayUntil, canRunNextCheckpoint, allCompletedSubTasks } = handleTaskResult;
|
|
1450
|
+
partNotificationTaskCompletionType = completion;
|
|
1451
|
+
partSuccess = true;
|
|
1452
|
+
switch (completion) {
|
|
1453
|
+
case true:
|
|
1454
|
+
notificationTemplate.d = true; // mark as done
|
|
1455
|
+
break;
|
|
1456
|
+
case false:
|
|
1457
|
+
// failed
|
|
1458
|
+
notificationTemplate.a = notification.a + 1; // increase attempts count
|
|
1459
|
+
notificationTemplate.at = (notification.at ?? 0) + 1; // increase checkpoint attempts count
|
|
1460
|
+
// remove any completions, if applicable
|
|
1461
|
+
notificationTemplate.tpr = removeFromCompletionsArrayWithTaskResult(notification.tpr, handleTaskResult);
|
|
1462
|
+
partSuccess = false;
|
|
1463
|
+
break;
|
|
1464
|
+
default:
|
|
1465
|
+
// default case called if not true or false, which implies either a delay or partial completion
|
|
1466
|
+
// update the checkpoint attempts count
|
|
1467
|
+
if (Array.isArray(completion) && completion.length === 0) {
|
|
1468
|
+
notificationTemplate.at = (notification.at ?? 0) + 1; // increase checkpoint attempt/delays count
|
|
1469
|
+
tryRunNextPart = canRunNextCheckpoint === true && allCompletedSubTasks != null && delayUntil == null; // can only run the next part if subtasks were returned and there is no delay
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
tryRunNextPart = canRunNextCheckpoint === true && delayUntil == null; // can try the next part if there is no delayUntil and canRunNextCheckpoint is true
|
|
1473
|
+
notificationTemplate.at = 0; // reset checkpoint attempt/delay count
|
|
1474
|
+
}
|
|
1475
|
+
// add the checkpoint to the notification
|
|
1476
|
+
notificationTemplate.tpr = [
|
|
1477
|
+
...removeFromCompletionsArrayWithTaskResult(notification.tpr, handleTaskResult), // remove any completions, if applicable
|
|
1478
|
+
...util.asArray(completion)
|
|
1479
|
+
];
|
|
1480
|
+
// calculate the updated notification item
|
|
1481
|
+
notificationTemplate.n = {
|
|
1482
|
+
...notification.n,
|
|
1483
|
+
d: {
|
|
1484
|
+
...notification.n.d,
|
|
1485
|
+
...(updateMetadata ? util.filterOnlyUndefinedValues(updateMetadata) : undefined) // ignore any undefined values
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
// can only run the next part if the tpr has changed, and the number of checkpoints completed has increased
|
|
1489
|
+
// if the tpr has not changed, then it is also considered a reversal
|
|
1490
|
+
if (tryRunNextPart) {
|
|
1491
|
+
const tprChanged = !util.iterablesAreSetEquivalent(notification.tpr, notificationTemplate.tpr);
|
|
1492
|
+
partTprReversal = !tprChanged || (tprChanged && notificationTemplate.tpr.length <= notification.tpr.length);
|
|
1493
|
+
if (allCompletedSubTasks != null) {
|
|
1494
|
+
switch (allCompletedSubTasks) {
|
|
1495
|
+
case true:
|
|
1496
|
+
case false:
|
|
1497
|
+
// only run if there is no tpr reversal flagged
|
|
1498
|
+
tryRunNextPart = !partTprReversal;
|
|
1499
|
+
break;
|
|
1500
|
+
default:
|
|
1501
|
+
// check subtask tpr changes
|
|
1502
|
+
nextCompleteSubTasks = util.asArray(allCompletedSubTasks);
|
|
1503
|
+
const subtaskTprChanged = !util.iterablesAreSetEquivalent(previouslyCompleteSubTasks, nextCompleteSubTasks);
|
|
1504
|
+
partTprReversal = !subtaskTprChanged || (subtaskTprChanged && nextCompleteSubTasks.length <= previouslyCompleteSubTasks.length);
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
// do not update sat if the task is complete
|
|
1512
|
+
if (completion !== true && delayUntil != null) {
|
|
1513
|
+
// must be at least 20 seconds into the future from now, and/or the nextSat time to avoid parallel runs
|
|
1514
|
+
const minimumNextSatTime = dateFns.addSeconds(new Date(), 20);
|
|
1515
|
+
notificationTemplate.sat = date.findMaxDate([util.dateOrMillisecondsToDate(delayUntil, now), nextSat, minimumNextSatTime]) ?? minimumNextSatTime;
|
|
1516
|
+
}
|
|
1517
|
+
partNotificationMarkedDone = notificationTemplate.d === true;
|
|
1518
|
+
}
|
|
1519
|
+
catch (e) {
|
|
1520
|
+
console.error(`Failed handling task for notification "${notification.key}" with type "${notificationTask.taskType}": `, e);
|
|
1521
|
+
notificationTemplate.a = notification.a + 1; // increase attempts count
|
|
1522
|
+
notificationTemplate.sat = util.dateOrMillisecondsToDate(NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS, now);
|
|
1523
|
+
partSuccess = false;
|
|
1524
|
+
}
|
|
1525
|
+
// notification tasks are read
|
|
1526
|
+
let saveTaskResult = true;
|
|
1527
|
+
if (unique) {
|
|
1528
|
+
isUniqueNotificationTask = true;
|
|
1529
|
+
const latestNotification = await notificationDocument.snapshotData();
|
|
1530
|
+
if (!latestNotification || !date.isSameDate(latestNotification.cat, notification.cat)) {
|
|
1531
|
+
saveTaskResult = false;
|
|
1532
|
+
uniqueNotificationTaskConflict = true;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (saveTaskResult) {
|
|
1536
|
+
await notificationDocument.update(notificationTemplate);
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
tryRunNextPart,
|
|
1540
|
+
partNotificationMarkedDone,
|
|
1541
|
+
partNotificationTaskCompletionType,
|
|
1542
|
+
partTprReversal,
|
|
1543
|
+
nextCompleteSubTasks,
|
|
1544
|
+
partSuccess
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Notification task handling.
|
|
1549
|
+
*
|
|
1550
|
+
* Notification takss can have multiple async but sequential parts.
|
|
1551
|
+
*
|
|
1552
|
+
* Some of these parts may be able to be run immediately one after the other, instead of waiting for
|
|
1553
|
+
* another sendNotification() to complete on it.
|
|
1554
|
+
*/
|
|
1555
|
+
async function handleNotificationTask() {
|
|
1556
|
+
const MAX_NOTIFICATION_TASK_PARTS_RUN_ALLOWED = 5;
|
|
1557
|
+
if (tryRun && notification != null && notificationTaskHandler) {
|
|
1558
|
+
let currentNotification = notification;
|
|
1559
|
+
let previouslyCompleteSubTasks = [];
|
|
1560
|
+
notificationTaskLoopingProtectionTriggered = false;
|
|
1561
|
+
notificationTaskPartsRunCount = 0;
|
|
1562
|
+
while (notificationTaskPartsRunCount < MAX_NOTIFICATION_TASK_PARTS_RUN_ALLOWED) {
|
|
1563
|
+
notificationTaskPartsRunCount += 1;
|
|
1564
|
+
const result = await _runNotificationTaskNextPart({
|
|
1565
|
+
notification: currentNotification,
|
|
1566
|
+
notificationTaskHandler,
|
|
1567
|
+
previouslyCompleteSubTasks
|
|
1568
|
+
});
|
|
1569
|
+
notificationTaskCompletionType = result.partNotificationTaskCompletionType;
|
|
1570
|
+
previouslyCompleteSubTasks = result.nextCompleteSubTasks ?? [];
|
|
1571
|
+
success = result.partSuccess;
|
|
1572
|
+
const tryRunNextPart = result.partSuccess && result.tryRunNextPart && !notificationTaskLoopingProtectionTriggered;
|
|
1573
|
+
notificationTaskLoopingProtectionTriggered = notificationTaskLoopingProtectionTriggered || result.partTprReversal; // update the flag if the TPR has been reversed
|
|
1574
|
+
if (tryRunNextPart) {
|
|
1575
|
+
const updatedNotificationData = await notificationDocument.snapshotData();
|
|
1576
|
+
if (updatedNotificationData) {
|
|
1577
|
+
currentNotification = firebase.setIdAndKeyFromKeyIdRefOnDocumentData(updatedNotificationData, notificationDocument);
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
break; // notification is unavailable now
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
break; // escape the loop
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Handles a normal (non-task) notification.
|
|
1591
|
+
*/
|
|
1592
|
+
async function handleNormalNotification() {
|
|
1593
|
+
// notification is only null/undefined if it didn't exist.
|
|
1594
|
+
if (notification != null) {
|
|
1595
|
+
if (tryRun && templateInstance != null) {
|
|
1596
|
+
// first load the message function
|
|
1597
|
+
const messageFunction = await templateInstance
|
|
1598
|
+
.loadMessageFunction({
|
|
1599
|
+
item: notification.n,
|
|
1600
|
+
notification,
|
|
1601
|
+
notificationBox: {
|
|
1602
|
+
m: notificationBoxModelKey
|
|
1603
|
+
}
|
|
1604
|
+
})
|
|
1605
|
+
.catch((e) => {
|
|
1606
|
+
loadMessageFunctionFailure = true;
|
|
1607
|
+
success = false;
|
|
1608
|
+
console.error(`Failed loading message function for type ${notificationTemplateType}: `, e);
|
|
1609
|
+
return undefined;
|
|
1610
|
+
});
|
|
1611
|
+
if (messageFunction) {
|
|
1612
|
+
function filterOutNoContentNotificationMessages(messages) {
|
|
1613
|
+
return messages.filter((x) => !x.flag);
|
|
1614
|
+
}
|
|
1615
|
+
// expand recipients
|
|
1616
|
+
const { emails: emailRecipients, texts: textRecipients, notificationSummaries: notificationSummaryRecipients } = await expandNotificationRecipients({
|
|
1617
|
+
notification,
|
|
1618
|
+
notificationBox,
|
|
1619
|
+
authService,
|
|
1620
|
+
notificationUserAccessor,
|
|
1621
|
+
globalRecipients: messageFunction.globalRecipients,
|
|
1622
|
+
onlySendToExplicitlyEnabledRecipients,
|
|
1623
|
+
onlyTextExplicitlyEnabledRecipients,
|
|
1624
|
+
notificationSummaryIdForUid: notificationSendService.notificationSummaryIdForUidFunction
|
|
1625
|
+
});
|
|
1626
|
+
let { es, ts, ps, ns, esr: currentEsr, tsr: currentTsr } = notification;
|
|
1627
|
+
// do emails
|
|
1628
|
+
let esr;
|
|
1629
|
+
if (es === firebase.NotificationSendState.QUEUED || es === firebase.NotificationSendState.SENT_PARTIAL) {
|
|
1630
|
+
const emailRecipientsAlreadySentTo = new Set(currentEsr.map((x) => x.toLowerCase()));
|
|
1631
|
+
const emailInputContexts = emailRecipients
|
|
1632
|
+
.filter((x) => !emailRecipientsAlreadySentTo.has(x.emailAddress.toLowerCase()))
|
|
1633
|
+
.map((x) => {
|
|
1634
|
+
const context = {
|
|
1635
|
+
recipient: {
|
|
1636
|
+
n: x.name,
|
|
1637
|
+
e: x.emailAddress,
|
|
1638
|
+
t: x.phoneNumber
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
return context;
|
|
1642
|
+
});
|
|
1643
|
+
const emailMessages = await Promise.all(emailInputContexts.map(messageFunction))
|
|
1644
|
+
.then(filterOutNoContentNotificationMessages)
|
|
1645
|
+
.catch((e) => {
|
|
1646
|
+
console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
|
|
1647
|
+
buildMessageFailure = true;
|
|
1648
|
+
return undefined;
|
|
1649
|
+
});
|
|
1650
|
+
if (emailMessages?.length) {
|
|
1651
|
+
if (notificationSendService.emailSendService != null) {
|
|
1652
|
+
let sendInstance;
|
|
1653
|
+
try {
|
|
1654
|
+
sendInstance = await notificationSendService.emailSendService.buildSendInstanceForEmailNotificationMessages(emailMessages);
|
|
1655
|
+
}
|
|
1656
|
+
catch (e) {
|
|
1657
|
+
console.error(`Failed building email send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1658
|
+
es = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1659
|
+
}
|
|
1660
|
+
if (sendInstance) {
|
|
1661
|
+
try {
|
|
1662
|
+
sendEmailsResult = await sendInstance();
|
|
1663
|
+
}
|
|
1664
|
+
catch (e) {
|
|
1665
|
+
console.error(`Failed sending email notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1666
|
+
es = firebase.NotificationSendState.SEND_ERROR;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
console.error(`Failed sending email notification "${notification.id}" with type "${notificationTemplateType}" due to no email service being configured.`);
|
|
1672
|
+
es = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1673
|
+
}
|
|
1674
|
+
if (sendEmailsResult != null) {
|
|
1675
|
+
const { success, failed } = sendEmailsResult;
|
|
1676
|
+
esr = success.length ? currentEsr.concat(success.map((x) => x.toLowerCase())) : undefined;
|
|
1677
|
+
if (failed.length > 0) {
|
|
1678
|
+
es = firebase.NotificationSendState.SENT_PARTIAL;
|
|
1679
|
+
}
|
|
1680
|
+
else {
|
|
1681
|
+
es = firebase.NotificationSendState.SENT;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
else {
|
|
1686
|
+
es = firebase.NotificationSendState.SENT;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
// do phone numbers
|
|
1690
|
+
let tsr;
|
|
1691
|
+
if (ts === firebase.NotificationSendState.QUEUED || ts === firebase.NotificationSendState.SENT_PARTIAL) {
|
|
1692
|
+
const textRecipientsAlreadySentTo = new Set(currentTsr);
|
|
1693
|
+
const textInputContexts = textRecipients
|
|
1694
|
+
.filter((x) => !textRecipientsAlreadySentTo.has(x.phoneNumber))
|
|
1695
|
+
.map((x) => {
|
|
1696
|
+
const context = {
|
|
1697
|
+
recipient: {
|
|
1698
|
+
n: x.name,
|
|
1699
|
+
e: x.emailAddress,
|
|
1700
|
+
t: x.phoneNumber
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
return context;
|
|
1704
|
+
});
|
|
1705
|
+
const textMessages = await Promise.all(textInputContexts.map(messageFunction))
|
|
1706
|
+
.then(filterOutNoContentNotificationMessages)
|
|
1707
|
+
.catch((e) => {
|
|
1708
|
+
console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
|
|
1709
|
+
buildMessageFailure = true;
|
|
1710
|
+
return undefined;
|
|
1711
|
+
});
|
|
1712
|
+
if (textMessages?.length) {
|
|
1713
|
+
if (notificationSendService.textSendService != null) {
|
|
1714
|
+
let sendInstance;
|
|
1715
|
+
try {
|
|
1716
|
+
sendInstance = await notificationSendService.textSendService.buildSendInstanceForTextNotificationMessages(textMessages);
|
|
1717
|
+
}
|
|
1718
|
+
catch (e) {
|
|
1719
|
+
console.error(`Failed building text send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1720
|
+
ts = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1721
|
+
}
|
|
1722
|
+
if (sendInstance) {
|
|
1723
|
+
try {
|
|
1724
|
+
sendTextsResult = await sendInstance();
|
|
1725
|
+
}
|
|
1726
|
+
catch (e) {
|
|
1727
|
+
console.error(`Failed sending text notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1728
|
+
ts = firebase.NotificationSendState.SEND_ERROR;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
console.error(`Failed sending text notification "${notification.id}" with type "${notificationTemplateType}" due to no text service being configured.`);
|
|
1734
|
+
ts = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1735
|
+
}
|
|
1736
|
+
if (sendTextsResult != null) {
|
|
1737
|
+
const { success, failed } = sendTextsResult;
|
|
1738
|
+
tsr = success.length ? currentTsr.concat(success) : undefined;
|
|
1739
|
+
if (failed.length > 0) {
|
|
1740
|
+
ts = firebase.NotificationSendState.SENT_PARTIAL;
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
ts = firebase.NotificationSendState.SENT;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
else {
|
|
1748
|
+
ts = firebase.NotificationSendState.SENT;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
ps = firebase.NotificationSendState.NO_TRY;
|
|
1752
|
+
// NOTE: FCM token management will probably done with a separate system within Notification that stores FCMs for specific users in the app. May also use UIDs to determine who got the push notificdation or not...
|
|
1753
|
+
// do notification summaries
|
|
1754
|
+
if (ns === firebase.NotificationSendState.QUEUED || ns === firebase.NotificationSendState.SENT_PARTIAL) {
|
|
1755
|
+
const notificationSummaryInputContexts = notificationSummaryRecipients.map((x) => {
|
|
1756
|
+
const context = {
|
|
1757
|
+
recipient: {
|
|
1758
|
+
n: x.name,
|
|
1759
|
+
s: x.notificationSummaryId
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
return context;
|
|
1763
|
+
});
|
|
1764
|
+
const notificationSummaryMessages = await Promise.all(notificationSummaryInputContexts.map(messageFunction))
|
|
1765
|
+
.then(filterOutNoContentNotificationMessages)
|
|
1766
|
+
.catch((e) => {
|
|
1767
|
+
console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
|
|
1768
|
+
buildMessageFailure = true;
|
|
1769
|
+
return undefined;
|
|
1770
|
+
});
|
|
1771
|
+
if (notificationSummaryMessages?.length) {
|
|
1772
|
+
if (notificationSendService.notificationSummarySendService != null) {
|
|
1773
|
+
let sendInstance;
|
|
1774
|
+
try {
|
|
1775
|
+
sendInstance = await notificationSendService.notificationSummarySendService.buildSendInstanceForNotificationSummaryMessages(notificationSummaryMessages);
|
|
1776
|
+
}
|
|
1777
|
+
catch (e) {
|
|
1778
|
+
console.error(`Failed building notification summary send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1779
|
+
ns = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1780
|
+
}
|
|
1781
|
+
if (sendInstance) {
|
|
1782
|
+
try {
|
|
1783
|
+
sendNotificationSummaryResult = await sendInstance();
|
|
1784
|
+
ns = firebase.NotificationSendState.SENT;
|
|
1785
|
+
}
|
|
1786
|
+
catch (e) {
|
|
1787
|
+
console.error(`Failed sending notification summary notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1788
|
+
ns = firebase.NotificationSendState.SEND_ERROR;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
console.error(`Failed sending notification summary notification "${notification.id}" with type "${notificationTemplateType}" due to no notification summary service being configured.`);
|
|
1794
|
+
ns = firebase.NotificationSendState.CONFIG_ERROR;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
ns = firebase.NotificationSendState.SENT;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
// calculate results
|
|
1802
|
+
const notificationTemplate = { es, ts, ps, ns, esr, tsr };
|
|
1803
|
+
success = firebase.notificationSendFlagsImplyIsComplete(notificationTemplate);
|
|
1804
|
+
if (success) {
|
|
1805
|
+
notificationTemplate.d = true;
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
notificationTemplate.a = notification.a + 1;
|
|
1809
|
+
if (notificationTemplate.a >= NOTIFICATION_MAX_SEND_ATTEMPTS) {
|
|
1810
|
+
notificationTemplate.d = true;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
await notificationDocument.update(notificationTemplate);
|
|
1814
|
+
notificationMarkedDone = notificationTemplate.d === true;
|
|
1815
|
+
const callbackDetails = {
|
|
1816
|
+
success,
|
|
1817
|
+
updatedSendFlags: notificationTemplate,
|
|
1818
|
+
sendEmailsResult,
|
|
1819
|
+
sendTextsResult,
|
|
1820
|
+
sendNotificationSummaryResult
|
|
1821
|
+
};
|
|
1822
|
+
const { onSendAttempted, onSendSuccess } = messageFunction;
|
|
1823
|
+
// call onSendAttempted, if one is configured
|
|
1824
|
+
if (onSendAttempted) {
|
|
1825
|
+
onSendAttemptedResult = await util.asPromise(onSendAttempted(callbackDetails))
|
|
1826
|
+
.then((value) => {
|
|
1827
|
+
return { value };
|
|
1828
|
+
})
|
|
1829
|
+
.catch((e) => {
|
|
1830
|
+
console.warn(`Caught exception while calling onSendAttempted for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1831
|
+
return { error: e };
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
// call onSendSuccess, if one is configured
|
|
1835
|
+
if (notificationMarkedDone && onSendSuccess) {
|
|
1836
|
+
onSendSuccessResult = await util.asPromise(onSendSuccess(callbackDetails))
|
|
1837
|
+
.then((value) => {
|
|
1838
|
+
return { value };
|
|
1839
|
+
})
|
|
1840
|
+
.catch((e) => {
|
|
1841
|
+
console.warn(`Caught exception while calling onSendSuccess for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
|
|
1842
|
+
return { error: e };
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
switch (notification.st) {
|
|
1849
|
+
case firebase.NotificationSendType.SEND_IF_BOX_EXISTS:
|
|
1850
|
+
// deleted successfully
|
|
1851
|
+
success = deletedNotification;
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const result = {
|
|
1858
|
+
notificationTemplateType,
|
|
1859
|
+
isKnownTemplateType,
|
|
1860
|
+
isNotificationTask,
|
|
1861
|
+
isUniqueNotificationTask,
|
|
1862
|
+
uniqueNotificationTaskConflict,
|
|
1863
|
+
isConfiguredTemplateType,
|
|
1864
|
+
throttled,
|
|
1865
|
+
exists: notification != null,
|
|
1866
|
+
boxExists: notificationBox != null,
|
|
1867
|
+
notificationTaskPartsRunCount,
|
|
1868
|
+
notificationTaskLoopingProtectionTriggered,
|
|
1869
|
+
notificationBoxNeedsInitialization,
|
|
1870
|
+
notificationTaskCompletionType,
|
|
1871
|
+
createdBox,
|
|
1872
|
+
deletedNotification,
|
|
1873
|
+
notificationMarkedDone,
|
|
1874
|
+
tryRun,
|
|
1875
|
+
success,
|
|
1876
|
+
sendEmailsResult,
|
|
1877
|
+
sendTextsResult,
|
|
1878
|
+
sendNotificationSummaryResult,
|
|
1879
|
+
loadMessageFunctionFailure,
|
|
1880
|
+
buildMessageFailure,
|
|
1881
|
+
onSendAttemptedResult,
|
|
1882
|
+
onSendSuccessResult
|
|
1883
|
+
};
|
|
1884
|
+
return result;
|
|
1885
|
+
};
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
const SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD = 5000;
|
|
1889
|
+
function sendQueuedNotificationsFactory(context) {
|
|
1890
|
+
const { firebaseServerActionTransformFunctionFactory, notificationCollectionGroup } = context;
|
|
1891
|
+
const sendNotification = sendNotificationFactory(context);
|
|
1892
|
+
return firebaseServerActionTransformFunctionFactory(firebase.SendQueuedNotificationsParams, async (params) => {
|
|
1893
|
+
const { maxSendNotificationLoops } = params;
|
|
1894
|
+
const maxLoops = maxSendNotificationLoops ?? Number.MAX_SAFE_INTEGER;
|
|
1895
|
+
const sendNotificationLoopsTaskExcessThreshold = params.sendNotificationLoopsTaskExcessThreshold ?? SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD;
|
|
1896
|
+
return async (input) => {
|
|
1897
|
+
const maxParallelTasks = input?.maxParellelSendTasks ?? params.maxParellelSendTasks ?? 5;
|
|
1898
|
+
const onSendNotificationResult = input?.onSendNotificationResult ?? util.mapIdentityFunction();
|
|
1899
|
+
let notificationLoopCount = 0;
|
|
1900
|
+
let notificationBoxesCreated = 0;
|
|
1901
|
+
let notificationsDeleted = 0;
|
|
1902
|
+
let notificationTasksVisited = 0;
|
|
1903
|
+
let notificationsVisited = 0;
|
|
1904
|
+
let notificationsSucceeded = 0;
|
|
1905
|
+
let notificationsDelayed = 0;
|
|
1906
|
+
let notificationsFailed = 0;
|
|
1907
|
+
let sendEmailsResult;
|
|
1908
|
+
let sendTextsResult;
|
|
1909
|
+
let sendNotificationSummaryResult;
|
|
1910
|
+
const sendNotificationParams = { key: firebase.firestoreDummyKey(), throwErrorIfSent: false };
|
|
1911
|
+
const sendNotificationInstance = await sendNotification(sendNotificationParams);
|
|
1912
|
+
let excessLoopsDetected = false;
|
|
1913
|
+
const sendQueuedNotifications = async () => {
|
|
1914
|
+
const query = notificationCollectionGroup.queryDocument(firebase.notificationsPastSendAtTimeQuery());
|
|
1915
|
+
const notificationDocuments = await query.getDocs();
|
|
1916
|
+
const result = await util.performAsyncTasks(notificationDocuments, async (notificationDocument) => {
|
|
1917
|
+
const result = await sendNotificationInstance(notificationDocument);
|
|
1918
|
+
onSendNotificationResult(result, notificationDocument);
|
|
1919
|
+
return result;
|
|
1920
|
+
}, {
|
|
1921
|
+
maxParallelTasks
|
|
1922
|
+
});
|
|
1923
|
+
return result;
|
|
1924
|
+
};
|
|
1925
|
+
// iterate through all notification items that need to be synced
|
|
1926
|
+
while (notificationLoopCount < maxLoops) {
|
|
1927
|
+
const sendQueuedNotificationsResults = await sendQueuedNotifications();
|
|
1928
|
+
sendQueuedNotificationsResults.results.forEach((x) => {
|
|
1929
|
+
const result = x[1];
|
|
1930
|
+
if (result.success) {
|
|
1931
|
+
notificationsSucceeded += 1;
|
|
1932
|
+
}
|
|
1933
|
+
else if (result.createdBox || result.notificationBoxNeedsInitialization) {
|
|
1934
|
+
notificationsDelayed += 1;
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
notificationsFailed += 1;
|
|
1938
|
+
}
|
|
1939
|
+
if (result.isNotificationTask) {
|
|
1940
|
+
notificationTasksVisited += 1;
|
|
1941
|
+
}
|
|
1942
|
+
if (result.deletedNotification) {
|
|
1943
|
+
notificationsDeleted += 1;
|
|
1944
|
+
}
|
|
1945
|
+
if (result.createdBox) {
|
|
1946
|
+
notificationBoxesCreated += 1;
|
|
1947
|
+
}
|
|
1948
|
+
sendEmailsResult = firebase.mergeNotificationSendMessagesResult(sendEmailsResult, result.sendEmailsResult);
|
|
1949
|
+
sendTextsResult = firebase.mergeNotificationSendMessagesResult(sendTextsResult, result.sendTextsResult);
|
|
1950
|
+
sendNotificationSummaryResult = firebase.mergeNotificationSendMessagesResult(sendNotificationSummaryResult, result.sendNotificationSummaryResult);
|
|
1951
|
+
});
|
|
1952
|
+
const found = sendQueuedNotificationsResults.results.length;
|
|
1953
|
+
notificationsVisited += found;
|
|
1954
|
+
notificationLoopCount += 1;
|
|
1955
|
+
if (!found) {
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
else if (!excessLoopsDetected && notificationLoopCount > sendNotificationLoopsTaskExcessThreshold) {
|
|
1959
|
+
excessLoopsDetected = true;
|
|
1960
|
+
console.error(`sendQueuedNotifications(EXCESS_LOOPS_DETECTED): Exceeded send notification loops task excess threshold of ${sendNotificationLoopsTaskExcessThreshold}.`);
|
|
1961
|
+
// continue the loops
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const result = {
|
|
1965
|
+
excessLoopsDetected,
|
|
1966
|
+
notificationLoopCount,
|
|
1967
|
+
notificationBoxesCreated,
|
|
1968
|
+
notificationsDeleted,
|
|
1969
|
+
notificationTasksVisited,
|
|
1970
|
+
notificationsVisited,
|
|
1971
|
+
notificationsSucceeded,
|
|
1972
|
+
notificationsDelayed,
|
|
1973
|
+
notificationsFailed,
|
|
1974
|
+
sendEmailsResult,
|
|
1975
|
+
sendTextsResult,
|
|
1976
|
+
sendNotificationSummaryResult
|
|
1977
|
+
};
|
|
1978
|
+
return result;
|
|
1979
|
+
};
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
function cleanupSentNotificationsFactory(context) {
|
|
1983
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationCollectionGroup, notificationBoxCollection, notificationWeekCollectionFactory } = context;
|
|
1984
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CleanupSentNotificationsParams, async () => {
|
|
1985
|
+
return async () => {
|
|
1986
|
+
let notificationBoxesUpdatesCount = 0;
|
|
1987
|
+
let notificationsDeleted = 0;
|
|
1988
|
+
const notificationTasksDeletedCount = 0;
|
|
1989
|
+
let notificationWeeksCreated = 0;
|
|
1990
|
+
let notificationWeeksUpdated = 0;
|
|
1991
|
+
// iterate through all Notification items that need to be cleaned up
|
|
1992
|
+
while (true) {
|
|
1993
|
+
const cleanupSentNotificationsResults = await cleanupSentNotifications();
|
|
1994
|
+
cleanupSentNotificationsResults.results.forEach((x) => {
|
|
1995
|
+
const { itemsDeleted, weeksCreated, weeksUpdated } = x[1];
|
|
1996
|
+
notificationsDeleted += itemsDeleted;
|
|
1997
|
+
notificationWeeksCreated += weeksCreated;
|
|
1998
|
+
notificationWeeksUpdated += weeksUpdated;
|
|
1999
|
+
});
|
|
2000
|
+
const notificationBoxesUpdated = cleanupSentNotificationsResults.results.length;
|
|
2001
|
+
notificationBoxesUpdatesCount += notificationBoxesUpdated;
|
|
2002
|
+
if (!notificationBoxesUpdated) {
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
async function cleanupSentNotifications() {
|
|
2007
|
+
const query = notificationCollectionGroup.queryDocument(firebase.notificationsReadyForCleanupQuery());
|
|
2008
|
+
const notificationDocuments = await query.getDocs();
|
|
2009
|
+
const notificationDocumentsGroupedByNotificationBox = Array.from(util.makeValuesGroupMap(notificationDocuments, (x) => x.parent.id).values());
|
|
2010
|
+
const result = await util.performAsyncTasks(notificationDocumentsGroupedByNotificationBox, async (notificationDocumentsInSameBox) => {
|
|
2011
|
+
const allPairs = await firebase.getDocumentSnapshotDataPairs(notificationDocumentsInSameBox);
|
|
2012
|
+
const allPairsWithDataAndMarkedDeleted = allPairs.filter((x) => x.data?.d);
|
|
2013
|
+
const { included: taskPairsWithDataAndMarkedDeleted, excluded: normalPairsWithDataAndMarkedDeleted } = util.separateValues(allPairsWithDataAndMarkedDeleted, (x) => x.data?.st === firebase.NotificationSendType.TASK_NOTIFICATION);
|
|
2014
|
+
const pairsGroupedByWeek = Array.from(util.makeValuesGroupMap(normalPairsWithDataAndMarkedDeleted, (x) => date.yearWeekCode(x.data.sat)).entries());
|
|
2015
|
+
// batch incase there are a lot of new notifications to move to week
|
|
2016
|
+
const pairsGroupedByWeekInBatches = pairsGroupedByWeek
|
|
2017
|
+
.map((x) => {
|
|
2018
|
+
const batches = util.batch(x[1], 40);
|
|
2019
|
+
return batches.map((batch) => [x[0], batch]);
|
|
2020
|
+
})
|
|
2021
|
+
.flat();
|
|
2022
|
+
const notificationBoxDocument = await notificationBoxCollection.documentAccessor().loadDocument(notificationDocumentsInSameBox[0].parent);
|
|
2023
|
+
// create/update the NotificationWeek
|
|
2024
|
+
const notificationWeekResults = await util.performAsyncTasks(pairsGroupedByWeekInBatches, async ([yearWeekCode, notificationDocumentsInSameWeek]) => {
|
|
2025
|
+
return firestoreContext.runTransaction(async (transaction) => {
|
|
2026
|
+
const notificationWeekDocument = notificationWeekCollectionFactory(notificationBoxDocument).documentAccessorForTransaction(transaction).loadDocumentForId(`${yearWeekCode}`);
|
|
2027
|
+
const notificationDocumentsInTransaction = firebase.loadDocumentsForDocumentReferencesFromValues(notificationCollectionGroup.documentAccessorForTransaction(transaction), notificationDocumentsInSameWeek, (x) => x.snapshot.ref);
|
|
2028
|
+
const notificationWeek = await notificationWeekDocument.snapshotData();
|
|
2029
|
+
const newItems = util.filterMaybeArrayValues(notificationDocumentsInSameWeek.map((x) => {
|
|
2030
|
+
const data = x.data;
|
|
2031
|
+
const shouldSaveToNotificationWeek = firebase.shouldSaveNotificationToNotificationWeek(data);
|
|
2032
|
+
return shouldSaveToNotificationWeek ? data.n : undefined;
|
|
2033
|
+
}));
|
|
2034
|
+
const n = [...(notificationWeek?.n ?? []), ...newItems];
|
|
2035
|
+
if (!notificationWeek) {
|
|
2036
|
+
// create
|
|
2037
|
+
await notificationWeekDocument.create({
|
|
2038
|
+
w: yearWeekCode,
|
|
2039
|
+
n
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
// update
|
|
2044
|
+
await notificationWeekDocument.update({
|
|
2045
|
+
n
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
// delete the notification items
|
|
2049
|
+
await Promise.all(notificationDocumentsInTransaction.map((x) => x.accessor.delete()));
|
|
2050
|
+
return {
|
|
2051
|
+
created: !notificationWeek
|
|
2052
|
+
};
|
|
2053
|
+
});
|
|
2054
|
+
});
|
|
2055
|
+
// delete all the task notifications
|
|
2056
|
+
const writeBatch = firestoreContext.batch();
|
|
2057
|
+
const writeBatchAccessor = notificationCollectionGroup.documentAccessorForTransaction(writeBatch);
|
|
2058
|
+
await Promise.all(taskPairsWithDataAndMarkedDeleted.map((x) => writeBatchAccessor.loadDocumentFrom(x.document).accessor.delete()));
|
|
2059
|
+
await writeBatch.commit();
|
|
2060
|
+
let weeksCreated = 0;
|
|
2061
|
+
let weeksUpdated = 0;
|
|
2062
|
+
const tasksDeleted = taskPairsWithDataAndMarkedDeleted.length;
|
|
2063
|
+
notificationWeekResults.results.forEach((x) => {
|
|
2064
|
+
if (x[1].created) {
|
|
2065
|
+
weeksCreated += 1;
|
|
2066
|
+
}
|
|
2067
|
+
else {
|
|
2068
|
+
weeksUpdated += 1;
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
const result = {
|
|
2072
|
+
weeksCreated,
|
|
2073
|
+
weeksUpdated,
|
|
2074
|
+
itemsDeleted: allPairsWithDataAndMarkedDeleted.length,
|
|
2075
|
+
tasksDeleted
|
|
2076
|
+
};
|
|
2077
|
+
return result;
|
|
2078
|
+
}, {
|
|
2079
|
+
maxParallelTasks: 10
|
|
2080
|
+
});
|
|
2081
|
+
return result;
|
|
2082
|
+
}
|
|
2083
|
+
const result = {
|
|
2084
|
+
notificationBoxesUpdatesCount,
|
|
2085
|
+
notificationTasksDeletedCount,
|
|
2086
|
+
notificationsDeleted,
|
|
2087
|
+
notificationWeeksCreated,
|
|
2088
|
+
notificationWeeksUpdated
|
|
2089
|
+
};
|
|
2090
|
+
return result;
|
|
2091
|
+
};
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// MARK: NotificationInitServerActionsContextConfig
|
|
2096
|
+
/**
|
|
2097
|
+
* Token to access/override the NotificationTemplateService's defaults records.
|
|
2098
|
+
*/
|
|
2099
|
+
const NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = 'NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG';
|
|
2100
|
+
const MAKE_TEMPLATE_FOR_NOTIFICATION_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = false;
|
|
2101
|
+
class NotificationInitServerActions {
|
|
2102
|
+
}
|
|
2103
|
+
function notificationInitServerActions(context) {
|
|
2104
|
+
return {
|
|
2105
|
+
initializeNotificationBox: initializeNotificationBoxFactory(context),
|
|
2106
|
+
initializeAllApplicableNotificationBoxes: initializeAllApplicableNotificationBoxesFactory(context),
|
|
2107
|
+
initializeNotificationSummary: initializeNotificationSummaryFactory(context),
|
|
2108
|
+
initializeAllApplicableNotificationSummaries: initializeAllApplicableNotificationSummariesFactory(context)
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
async function initializeNotificationModelInTransaction(input) {
|
|
2112
|
+
const { makeTemplateFunction, throwErrorIfAlreadyInitialized, transaction, document: documentInTransaction, data: notificationBox } = input;
|
|
2113
|
+
let initialized = false;
|
|
2114
|
+
const alreadyInitialized = !notificationBox.s;
|
|
2115
|
+
if (!alreadyInitialized) {
|
|
2116
|
+
const flatModelKey = documentInTransaction.id;
|
|
2117
|
+
const modelKey = firebase.inferKeyFromTwoWayFlatFirestoreModelKey(flatModelKey);
|
|
2118
|
+
const modelCollectionName = firebase.firestoreModelKeyCollectionName(modelKey);
|
|
2119
|
+
const input = {
|
|
2120
|
+
transaction,
|
|
2121
|
+
flatModelKey,
|
|
2122
|
+
modelKey,
|
|
2123
|
+
collectionName: modelCollectionName
|
|
2124
|
+
};
|
|
2125
|
+
const template = await makeTemplateFunction(input);
|
|
2126
|
+
if (template === false) {
|
|
2127
|
+
await documentInTransaction.accessor.delete();
|
|
2128
|
+
}
|
|
2129
|
+
else if (template == null) {
|
|
2130
|
+
await documentInTransaction.update({
|
|
2131
|
+
s: false, // set false when "f" is set true
|
|
2132
|
+
fi: true
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
initialized = true;
|
|
2137
|
+
await documentInTransaction.update({
|
|
2138
|
+
//
|
|
2139
|
+
...template,
|
|
2140
|
+
m: undefined, // should not be changed
|
|
2141
|
+
s: null, // is now initialized.
|
|
2142
|
+
fi: false // set false
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
else if (throwErrorIfAlreadyInitialized) {
|
|
2147
|
+
throw notificationModelAlreadyInitializedError();
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
initialized,
|
|
2151
|
+
alreadyInitialized
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function initializeNotificationBoxInTransactionFactory(context) {
|
|
2155
|
+
const { notificationBoxCollection, makeTemplateForNotificationBoxInitialization } = context;
|
|
2156
|
+
return async (params, notificationBoxDocument, transaction) => {
|
|
2157
|
+
const { throwErrorIfAlreadyInitialized } = params;
|
|
2158
|
+
const notificationBoxDocumentInTransaction = notificationBoxCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationBoxDocument);
|
|
2159
|
+
const notificationBox = await firebaseServer.assertSnapshotData(notificationBoxDocumentInTransaction);
|
|
2160
|
+
return initializeNotificationModelInTransaction({
|
|
2161
|
+
makeTemplateFunction: makeTemplateForNotificationBoxInitialization,
|
|
2162
|
+
throwErrorIfAlreadyInitialized,
|
|
2163
|
+
transaction,
|
|
2164
|
+
document: notificationBoxDocumentInTransaction,
|
|
2165
|
+
data: notificationBox
|
|
2166
|
+
});
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function initializeNotificationBoxFactory(context) {
|
|
2170
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
2171
|
+
const initializeNotificationBoxInTransaction = initializeNotificationBoxInTransactionFactory(context);
|
|
2172
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeNotificationModelParams, async (params) => {
|
|
2173
|
+
return async (notificationBoxDocument) => {
|
|
2174
|
+
await firestoreContext.runTransaction((transaction) => initializeNotificationBoxInTransaction(params, notificationBoxDocument, transaction));
|
|
2175
|
+
return notificationBoxDocument;
|
|
2176
|
+
};
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
function initializeAllApplicableNotificationBoxesFactory(context) {
|
|
2180
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationBoxCollection, notificationCollectionGroup } = context;
|
|
2181
|
+
const initializeNotificationBoxInTransaction = initializeNotificationBoxInTransactionFactory(context);
|
|
2182
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeAllApplicableNotificationBoxesParams, async () => {
|
|
2183
|
+
return async () => {
|
|
2184
|
+
let notificationBoxesVisited = 0;
|
|
2185
|
+
let notificationBoxesSucceeded = 0;
|
|
2186
|
+
let notificationBoxesFailed = 0;
|
|
2187
|
+
let notificationBoxesAlreadyInitialized = 0;
|
|
2188
|
+
const initializeNotificationBoxParams = { key: firebase.firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
|
|
2189
|
+
async function initializeNotificationBoxes() {
|
|
2190
|
+
const query = notificationBoxCollection.queryDocument(firebase.notificationBoxesFlaggedForNeedsInitializationQuery());
|
|
2191
|
+
const notificationBoxDocuments = await query.getDocs();
|
|
2192
|
+
const result = await util.performAsyncTasks(notificationBoxDocuments, async (notificationBoxDocument) => {
|
|
2193
|
+
return firestoreContext.runTransaction((transaction) => initializeNotificationBoxInTransaction(initializeNotificationBoxParams, notificationBoxDocument, transaction));
|
|
2194
|
+
}, {
|
|
2195
|
+
maxParallelTasks: 5
|
|
2196
|
+
});
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
// iterate through all NotificationBox items that need to be synced
|
|
2200
|
+
while (true) {
|
|
2201
|
+
const initializeNotificationBoxesResults = await initializeNotificationBoxes();
|
|
2202
|
+
initializeNotificationBoxesResults.results.forEach((x) => {
|
|
2203
|
+
const result = x[1];
|
|
2204
|
+
if (result.alreadyInitialized) {
|
|
2205
|
+
notificationBoxesAlreadyInitialized += 1;
|
|
2206
|
+
}
|
|
2207
|
+
else if (result.initialized) {
|
|
2208
|
+
notificationBoxesSucceeded += 1;
|
|
2209
|
+
}
|
|
2210
|
+
else {
|
|
2211
|
+
notificationBoxesFailed += 1;
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
const found = initializeNotificationBoxesResults.results.length;
|
|
2215
|
+
notificationBoxesVisited += found;
|
|
2216
|
+
if (!found) {
|
|
2217
|
+
break;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const result = {
|
|
2221
|
+
notificationBoxesVisited,
|
|
2222
|
+
notificationBoxesSucceeded,
|
|
2223
|
+
notificationBoxesFailed,
|
|
2224
|
+
notificationBoxesAlreadyInitialized
|
|
2225
|
+
};
|
|
2226
|
+
return result;
|
|
2227
|
+
};
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
function initializeNotificationSummaryInTransactionFactory(context) {
|
|
2231
|
+
const { notificationSummaryCollection, makeTemplateForNotificationSummaryInitialization } = context;
|
|
2232
|
+
return async (params, notificationSummaryDocument, transaction) => {
|
|
2233
|
+
const { throwErrorIfAlreadyInitialized } = params;
|
|
2234
|
+
const notificationSummaryDocumentInTransaction = notificationSummaryCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationSummaryDocument);
|
|
2235
|
+
const notificationSummary = await firebaseServer.assertSnapshotData(notificationSummaryDocumentInTransaction);
|
|
2236
|
+
return initializeNotificationModelInTransaction({
|
|
2237
|
+
makeTemplateFunction: makeTemplateForNotificationSummaryInitialization,
|
|
2238
|
+
throwErrorIfAlreadyInitialized,
|
|
2239
|
+
transaction,
|
|
2240
|
+
document: notificationSummaryDocumentInTransaction,
|
|
2241
|
+
data: notificationSummary
|
|
2242
|
+
});
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
function initializeNotificationSummaryFactory(context) {
|
|
2246
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
2247
|
+
const initializeNotificationSummaryInTransaction = initializeNotificationSummaryInTransactionFactory(context);
|
|
2248
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeNotificationModelParams, async (params) => {
|
|
2249
|
+
return async (notificationSummaryDocument) => {
|
|
2250
|
+
await firestoreContext.runTransaction((transaction) => initializeNotificationSummaryInTransaction(params, notificationSummaryDocument, transaction));
|
|
2251
|
+
return notificationSummaryDocument;
|
|
2252
|
+
};
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
function initializeAllApplicableNotificationSummariesFactory(context) {
|
|
2256
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationSummaryCollection, notificationCollectionGroup } = context;
|
|
2257
|
+
const initializeNotificationSummaryInTransaction = initializeNotificationSummaryInTransactionFactory(context);
|
|
2258
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeAllApplicableNotificationSummariesParams, async () => {
|
|
2259
|
+
return async () => {
|
|
2260
|
+
let notificationSummariesVisited = 0;
|
|
2261
|
+
let notificationSummariesSucceeded = 0;
|
|
2262
|
+
let notificationSummariesFailed = 0;
|
|
2263
|
+
let notificationSummariesAlreadyInitialized = 0;
|
|
2264
|
+
const initializeNotificationSummaryParams = { key: firebase.firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
|
|
2265
|
+
async function initializeNotificationSummaries() {
|
|
2266
|
+
const query = notificationSummaryCollection.queryDocument(firebase.notificationSummariesFlaggedForNeedsInitializationQuery());
|
|
2267
|
+
const notificationSummaryDocuments = await query.getDocs();
|
|
2268
|
+
const result = await util.performAsyncTasks(notificationSummaryDocuments, async (notificationSummaryDocument) => {
|
|
2269
|
+
return firestoreContext.runTransaction((transaction) => initializeNotificationSummaryInTransaction(initializeNotificationSummaryParams, notificationSummaryDocument, transaction));
|
|
2270
|
+
}, {
|
|
2271
|
+
maxParallelTasks: 5
|
|
2272
|
+
});
|
|
2273
|
+
return result;
|
|
2274
|
+
}
|
|
2275
|
+
// iterate through all NotificationSummary items that need to be synced
|
|
2276
|
+
while (true) {
|
|
2277
|
+
const initializeNotificationSummariesResults = await initializeNotificationSummaries();
|
|
2278
|
+
initializeNotificationSummariesResults.results.forEach((x) => {
|
|
2279
|
+
const result = x[1];
|
|
2280
|
+
if (result.alreadyInitialized) {
|
|
2281
|
+
notificationSummariesAlreadyInitialized += 1;
|
|
2282
|
+
}
|
|
2283
|
+
else if (result.initialized) {
|
|
2284
|
+
notificationSummariesSucceeded += 1;
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
notificationSummariesFailed += 1;
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
const found = initializeNotificationSummariesResults.results.length;
|
|
2291
|
+
notificationSummariesVisited += found;
|
|
2292
|
+
if (!found) {
|
|
2293
|
+
break;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const result = {
|
|
2297
|
+
notificationSummariesVisited,
|
|
2298
|
+
notificationSummariesSucceeded,
|
|
2299
|
+
notificationSummariesFailed,
|
|
2300
|
+
notificationSummariesAlreadyInitialized
|
|
2301
|
+
};
|
|
2302
|
+
return result;
|
|
2303
|
+
};
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/******************************************************************************
|
|
2308
|
+
Copyright (c) Microsoft Corporation.
|
|
2309
|
+
|
|
2310
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
2311
|
+
purpose with or without fee is hereby granted.
|
|
2312
|
+
|
|
2313
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
2314
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
2315
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
2316
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
2317
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
2318
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
2319
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
2320
|
+
***************************************************************************** */
|
|
2321
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
function __decorate(decorators, target, key, desc) {
|
|
2325
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2326
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2327
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2328
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function __param(paramIndex, decorator) {
|
|
2332
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
function __metadata(metadataKey, metadataValue) {
|
|
2336
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
2340
|
+
var e = new Error(message);
|
|
2341
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
// MARK: Tokens
|
|
2345
|
+
/**
|
|
2346
|
+
* Token to access/override the NotificationTemplateService's defaults records.
|
|
2347
|
+
*/
|
|
2348
|
+
const NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN = 'NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE';
|
|
2349
|
+
/**
|
|
2350
|
+
* Token to access the NotificationTemplateService's type configs array.
|
|
2351
|
+
*/
|
|
2352
|
+
const NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN = 'NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY';
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
|
|
2356
|
+
*/
|
|
2357
|
+
exports.NotificationTemplateService = class NotificationTemplateService {
|
|
2358
|
+
_defaults;
|
|
2359
|
+
_config;
|
|
2360
|
+
constructor(_inputDefaults, _inputConfigs) {
|
|
2361
|
+
this._defaults = _inputDefaults ?? {};
|
|
2362
|
+
this._config = new Map();
|
|
2363
|
+
if (_inputConfigs != null) {
|
|
2364
|
+
_inputConfigs.forEach((x) => {
|
|
2365
|
+
this._config.set(x.type, x);
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
configPairForType(type) {
|
|
2370
|
+
return [this._defaults[type], this._config.get(type)];
|
|
2371
|
+
}
|
|
2372
|
+
templateInstanceForType(type) {
|
|
2373
|
+
return notificationTemplateServiceInstance(this, type);
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
exports.NotificationTemplateService = __decorate([
|
|
2377
|
+
__param(0, common.Optional()),
|
|
2378
|
+
__param(0, common.Inject(NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN)),
|
|
2379
|
+
__param(1, common.Inject(NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN)),
|
|
2380
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
2381
|
+
], exports.NotificationTemplateService);
|
|
2382
|
+
/**
|
|
2383
|
+
* Creates a NotificationTemplateServiceInstance.
|
|
2384
|
+
*
|
|
2385
|
+
* @param service
|
|
2386
|
+
* @param type
|
|
2387
|
+
* @returns
|
|
2388
|
+
*/
|
|
2389
|
+
function notificationTemplateServiceInstance(service, type) {
|
|
2390
|
+
const pair = service.configPairForType(type);
|
|
2391
|
+
const isKnownType = pair[0] != null || pair[1] != null;
|
|
2392
|
+
const defaultFactory = firebase.noContentNotificationMessageFunctionFactory();
|
|
2393
|
+
const instanceConfig = pair[1];
|
|
2394
|
+
return {
|
|
2395
|
+
service,
|
|
2396
|
+
type,
|
|
2397
|
+
isConfiguredType: isKnownType,
|
|
2398
|
+
loadMessageFunction: async (config) => {
|
|
2399
|
+
const factory = instanceConfig?.factory ?? defaultFactory;
|
|
2400
|
+
return factory(config);
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
/**
|
|
2406
|
+
* Alternative version of createNotificationDocument() that checks if the document exists, and can run it if it does instead of recreated it.
|
|
2407
|
+
*
|
|
2408
|
+
* Does not support the use of a Transaction, as running should occur outside of a transaction.
|
|
2409
|
+
*
|
|
2410
|
+
* @param input
|
|
2411
|
+
* @returns
|
|
2412
|
+
*/
|
|
2413
|
+
async function createOrRunUniqueNotificationDocument(input) {
|
|
2414
|
+
const { expediteService, expediteInstance, updateNextRunAtTime, now: inputNow } = input;
|
|
2415
|
+
let sat = input.template.sat;
|
|
2416
|
+
if (updateNextRunAtTime != null) {
|
|
2417
|
+
sat = updateNextRunAtTime === true ? (inputNow ?? new Date()) : updateNextRunAtTime;
|
|
2418
|
+
}
|
|
2419
|
+
const pair = firebase.createNotificationDocumentPair({
|
|
2420
|
+
...input,
|
|
2421
|
+
template: {
|
|
2422
|
+
...input.template,
|
|
2423
|
+
sat
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
if (!pair.notification.ut) {
|
|
2427
|
+
throw new Error('createOrRunUniqueNotificationDocument(): Notification is not flagged as unique.');
|
|
2428
|
+
}
|
|
2429
|
+
const pairExists = await pair.notificationDocument.exists();
|
|
2430
|
+
let result = {
|
|
2431
|
+
...pair,
|
|
2432
|
+
notificationCreated: false
|
|
2433
|
+
};
|
|
2434
|
+
async function runNotificationTask() {
|
|
2435
|
+
if (expediteService != null) {
|
|
2436
|
+
result.runResult = await expediteService.sendNotification(pair.notificationDocument, input.sendNotificationOptions);
|
|
2437
|
+
}
|
|
2438
|
+
else if (expediteInstance != null) {
|
|
2439
|
+
expediteInstance.enqueue(pair.notificationDocument);
|
|
2440
|
+
result.runEnqueued = true;
|
|
2441
|
+
}
|
|
2442
|
+
else if (!result.notificationCreated && updateNextRunAtTime != null) {
|
|
2443
|
+
await pair.notificationDocument.update({
|
|
2444
|
+
sat: sat
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (pairExists) {
|
|
2449
|
+
await runNotificationTask();
|
|
2450
|
+
}
|
|
2451
|
+
else {
|
|
2452
|
+
result = await firebase._createNotificationDocumentFromPair(input, pair);
|
|
2453
|
+
if (result.notificationCreated && input.runImmediatelyIfCreated) {
|
|
2454
|
+
await runNotificationTask();
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return result;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
|
|
2462
|
+
*/
|
|
2463
|
+
class NotificationSendService {
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
|
|
2468
|
+
*/
|
|
2469
|
+
class NotificationTaskService {
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Interface for a service that allows access to a NotificationServerActions instance and "expediting" the sending of notification(s) that should be emitted immediately for timeliness.
|
|
2474
|
+
*
|
|
2475
|
+
* @see MutableNotificationExpediteService is the default implementation.
|
|
2476
|
+
*/
|
|
2477
|
+
class NotificationExpediteService {
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Creates a new NotificationExpediteServiceInstance with the input NotificationExpediteService.
|
|
2481
|
+
|
|
2482
|
+
* @param notificationExpediteService
|
|
2483
|
+
* @returns
|
|
2484
|
+
*/
|
|
2485
|
+
function notificationExpediteServiceInstance(notificationExpediteService) {
|
|
2486
|
+
let _documentsToSend = [];
|
|
2487
|
+
const initialize = () => {
|
|
2488
|
+
_documentsToSend = []; // resets the documents to send
|
|
2489
|
+
};
|
|
2490
|
+
const enqueue = (notificationDocument) => {
|
|
2491
|
+
_documentsToSend.push(notificationDocument);
|
|
2492
|
+
};
|
|
2493
|
+
const enqueueCreateResult = (createResult) => {
|
|
2494
|
+
let enqueued = false;
|
|
2495
|
+
if (createResult.notificationDocument) {
|
|
2496
|
+
enqueue(createResult.notificationDocument);
|
|
2497
|
+
enqueued = true;
|
|
2498
|
+
}
|
|
2499
|
+
return enqueued;
|
|
2500
|
+
};
|
|
2501
|
+
const send = async (options) => {
|
|
2502
|
+
const results = await util.runAsyncTasksForValues(_documentsToSend, (x) => notificationExpediteService.sendNotification(x, options), {
|
|
2503
|
+
nonConcurrentTaskKeyFactory: (x) => x.parent.id // only send one notification at a time for a notification box
|
|
2504
|
+
});
|
|
2505
|
+
return results;
|
|
2506
|
+
};
|
|
2507
|
+
return {
|
|
2508
|
+
initialize,
|
|
2509
|
+
enqueue,
|
|
2510
|
+
enqueueCreateResult,
|
|
2511
|
+
send
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
// MARK: Implementation
|
|
2515
|
+
/**
|
|
2516
|
+
* Service used to "expedite" the sending of a specific notification.
|
|
2517
|
+
*
|
|
2518
|
+
* Because the NotificationActionService is typically created after other action services are due to the dependency injection graph, this service is
|
|
2519
|
+
* created before the NotificationActionService is created, and then later updated by the NotificationActionService.
|
|
2520
|
+
*
|
|
2521
|
+
* It is best provided by provideMutableNotificationExpediteService() as a global provider.
|
|
2522
|
+
*/
|
|
2523
|
+
exports.MutableNotificationExpediteService = class MutableNotificationExpediteService {
|
|
2524
|
+
_notificationServerActions;
|
|
2525
|
+
/**
|
|
2526
|
+
* Returns the configured NotificationServerActions instance.
|
|
2527
|
+
*/
|
|
2528
|
+
getNotificationServerActions() {
|
|
2529
|
+
return this._notificationServerActions;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Sets the NotificationServerActions instance to use.
|
|
2533
|
+
*/
|
|
2534
|
+
setNotificationServerActions(notificationServerActions) {
|
|
2535
|
+
this._notificationServerActions = notificationServerActions;
|
|
2536
|
+
}
|
|
2537
|
+
async sendNotification(notificationDocument, options) {
|
|
2538
|
+
const sendNotification = await this._notificationServerActions.sendNotification({ key: firebase.firestoreDummyKey(), ...options });
|
|
2539
|
+
return sendNotification(notificationDocument);
|
|
2540
|
+
}
|
|
2541
|
+
expediteInstance() {
|
|
2542
|
+
return notificationExpediteServiceInstance(this);
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
exports.MutableNotificationExpediteService = __decorate([
|
|
2546
|
+
common.Injectable()
|
|
2547
|
+
], exports.MutableNotificationExpediteService);
|
|
2548
|
+
// MARK: Providers
|
|
2549
|
+
/**
|
|
2550
|
+
* Provides an instance of MutableNotificationExpediteService and NotificationExpediteService.
|
|
2551
|
+
*
|
|
2552
|
+
* This should generally be used in the global module of an app.
|
|
2553
|
+
*/
|
|
2554
|
+
function provideMutableNotificationExpediteService() {
|
|
2555
|
+
return [
|
|
2556
|
+
exports.MutableNotificationExpediteService,
|
|
2557
|
+
{
|
|
2558
|
+
provide: NotificationExpediteService,
|
|
2559
|
+
useExisting: exports.MutableNotificationExpediteService
|
|
2560
|
+
}
|
|
2561
|
+
];
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Convenience function that exports NotificationExpediteService and MutableNotificationExpediteService.
|
|
2565
|
+
*
|
|
2566
|
+
* This should generally be used in the global module of an app.
|
|
2567
|
+
*/
|
|
2568
|
+
function exportMutableNotificationExpediteService() {
|
|
2569
|
+
return [NotificationExpediteService, exports.MutableNotificationExpediteService];
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// MARK: Provider Factories
|
|
2573
|
+
function notificationServerActionsContextFactory(context, notificationTemplateService, notificationSendService, notificationTaskService, notificationsExpediteService) {
|
|
2574
|
+
return { ...context, notificationTemplateService, notificationSendService, notificationTaskService, notificationsExpediteService };
|
|
2575
|
+
}
|
|
2576
|
+
function notificationServerActionsFactory(context, mutableNotificationExpediteService) {
|
|
2577
|
+
return notificationServerActions(context);
|
|
2578
|
+
}
|
|
2579
|
+
function notificationInitServerActionsFactory(context, notificationInitServerActionsContextConfig) {
|
|
2580
|
+
return notificationInitServerActions({
|
|
2581
|
+
...context,
|
|
2582
|
+
...notificationInitServerActionsContextConfig
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Convenience function used to generate ModuleMetadata for an app's NotificationModule.
|
|
2587
|
+
*
|
|
2588
|
+
* By default this module exports:
|
|
2589
|
+
* - NotificationServerActionContext (NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN)
|
|
2590
|
+
* - NotificationTemplateService
|
|
2591
|
+
* - NotificationServerActions
|
|
2592
|
+
* - NotificationInitServerActions
|
|
2593
|
+
* - NotificationExpediteService (MutableNotificationExpediteService is used as the existing, but it is not re-exported)
|
|
2594
|
+
*
|
|
2595
|
+
* Be sure the class that delares the module using this function also extends AbstractAppNotificationModule.
|
|
2596
|
+
*
|
|
2597
|
+
* @param provide
|
|
2598
|
+
* @param useFactory
|
|
2599
|
+
* @returns
|
|
2600
|
+
*/
|
|
2601
|
+
function appNotificationModuleMetadata(config$1) {
|
|
2602
|
+
const { dependencyModule, imports, exports: exports$1, providers } = config$1;
|
|
2603
|
+
const dependencyModuleImport = dependencyModule ? [dependencyModule] : [];
|
|
2604
|
+
return {
|
|
2605
|
+
imports: [config.ConfigModule, ...dependencyModuleImport, ...(imports ?? [])],
|
|
2606
|
+
exports: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NotificationExpediteService, exports.NotificationTemplateService, NotificationServerActions, NotificationInitServerActions, ...(exports$1 ?? [])],
|
|
2607
|
+
providers: [
|
|
2608
|
+
{
|
|
2609
|
+
provide: NotificationExpediteService,
|
|
2610
|
+
useExisting: exports.MutableNotificationExpediteService
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
provide: NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN,
|
|
2614
|
+
useFactory: notificationServerActionsContextFactory,
|
|
2615
|
+
inject: [BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, exports.NotificationTemplateService, NotificationSendService, NotificationTaskService, NotificationExpediteService]
|
|
2616
|
+
},
|
|
2617
|
+
{
|
|
2618
|
+
provide: exports.NotificationTemplateService,
|
|
2619
|
+
useClass: exports.NotificationTemplateService
|
|
2620
|
+
},
|
|
2621
|
+
{
|
|
2622
|
+
provide: NotificationServerActions,
|
|
2623
|
+
useFactory: notificationServerActionsFactory,
|
|
2624
|
+
inject: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NotificationExpediteService]
|
|
2625
|
+
},
|
|
2626
|
+
{
|
|
2627
|
+
provide: NotificationInitServerActions,
|
|
2628
|
+
useFactory: notificationInitServerActionsFactory,
|
|
2629
|
+
inject: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN]
|
|
2630
|
+
},
|
|
2631
|
+
...(providers ?? [])
|
|
2632
|
+
]
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Abstract module that should be extended when using appNotificationModuleMetadata.
|
|
2637
|
+
*/
|
|
2638
|
+
exports.AbstractAppNotificationModule = class AbstractAppNotificationModule {
|
|
2639
|
+
constructor(mutableNotificationExpediteService, actions) {
|
|
2640
|
+
mutableNotificationExpediteService.setNotificationServerActions(actions);
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
exports.AbstractAppNotificationModule = __decorate([
|
|
2644
|
+
common.Module({}),
|
|
2645
|
+
__param(0, common.Inject(exports.MutableNotificationExpediteService)),
|
|
2646
|
+
__param(1, common.Inject(NotificationServerActions)),
|
|
2647
|
+
__metadata("design:paramtypes", [exports.MutableNotificationExpediteService, NotificationServerActions])
|
|
2648
|
+
], exports.AbstractAppNotificationModule);
|
|
2649
|
+
/**
|
|
2650
|
+
* Pre-configured global provider for MutableNotificationExpediteService/NotificationExpediteService.
|
|
2651
|
+
*/
|
|
2652
|
+
exports.GlobalNotificationModule = class GlobalNotificationModule {
|
|
2653
|
+
};
|
|
2654
|
+
exports.GlobalNotificationModule = __decorate([
|
|
2655
|
+
common.Global(),
|
|
2656
|
+
common.Module({
|
|
2657
|
+
providers: provideMutableNotificationExpediteService(),
|
|
2658
|
+
exports: exportMutableNotificationExpediteService()
|
|
2659
|
+
})
|
|
2660
|
+
], exports.GlobalNotificationModule);
|
|
2661
|
+
|
|
2662
|
+
function firestoreNotificationSummarySendService(config) {
|
|
2663
|
+
const { context, allowCreateNotificationSummaries: inputAllowCreateNotificationSummaries } = config;
|
|
2664
|
+
const { firestoreContext, notificationSummaryCollection } = context;
|
|
2665
|
+
const allowCreateNotificationSummaries = inputAllowCreateNotificationSummaries ?? true;
|
|
2666
|
+
const sendService = {
|
|
2667
|
+
async buildSendInstanceForNotificationSummaryMessages(notificationMessages) {
|
|
2668
|
+
const messagesGroupedByNotificationSummaryMapBuilder = util.multiValueMapBuilder();
|
|
2669
|
+
notificationMessages.forEach((x) => {
|
|
2670
|
+
if (x.inputContext.recipient.s != null && x.item != null) {
|
|
2671
|
+
// only add to map builder if recipient id is defined
|
|
2672
|
+
messagesGroupedByNotificationSummaryMapBuilder.add(x.inputContext.recipient.s, x);
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
const cutSubject = util.cutStringFunction({ maxLength: firebase.NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_SUBJECT_MAX_LENGTH });
|
|
2676
|
+
const cutMessage = util.cutStringFunction({ maxLength: firebase.NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_MESSAGE_MAX_LENGTH });
|
|
2677
|
+
const messagesGroups = messagesGroupedByNotificationSummaryMapBuilder.entries();
|
|
2678
|
+
return async () => {
|
|
2679
|
+
const success = [];
|
|
2680
|
+
const failed = [];
|
|
2681
|
+
const ignored = [];
|
|
2682
|
+
await util.runAsyncTasksForValues(messagesGroups, async ([notificationSummaryId, messages]) => {
|
|
2683
|
+
await firestoreContext
|
|
2684
|
+
.runTransaction(async (transaction) => {
|
|
2685
|
+
const notificationSummaryDocument = notificationSummaryCollection.documentAccessorForTransaction(transaction).loadDocumentForId(notificationSummaryId);
|
|
2686
|
+
const notificationSummary = await notificationSummaryDocument.snapshotData();
|
|
2687
|
+
let updated = false;
|
|
2688
|
+
let updateTemplate;
|
|
2689
|
+
const existingMessages = notificationSummary?.n ?? [];
|
|
2690
|
+
const existingMessageIds = new Set(existingMessages.map((x) => x.id));
|
|
2691
|
+
// ignore any repeat messages
|
|
2692
|
+
const messagesToSend = messages.filter((x) => !existingMessageIds.has(x.item.id));
|
|
2693
|
+
if (messagesToSend.length > 0) {
|
|
2694
|
+
// add the new items to existing n, then keep the last 1000
|
|
2695
|
+
const sortedN = existingMessages
|
|
2696
|
+
.concat(messagesToSend.map((x) => {
|
|
2697
|
+
let message = '';
|
|
2698
|
+
if (x.content.openingMessage) {
|
|
2699
|
+
message = x.content.openingMessage;
|
|
2700
|
+
}
|
|
2701
|
+
if (x.content.closingMessage) {
|
|
2702
|
+
message = (message ? message + '\n\n' : message) + x.content.closingMessage;
|
|
2703
|
+
}
|
|
2704
|
+
const item = {
|
|
2705
|
+
...x.item,
|
|
2706
|
+
s: cutSubject(x.content.title),
|
|
2707
|
+
g: cutMessage(message)
|
|
2708
|
+
};
|
|
2709
|
+
return item;
|
|
2710
|
+
}))
|
|
2711
|
+
.sort(firebase.sortNotificationItemsFunction);
|
|
2712
|
+
const n = util.takeLast(sortedN, firebase.NOTIFICATION_SUMMARY_ITEM_LIMIT);
|
|
2713
|
+
updateTemplate = {
|
|
2714
|
+
n,
|
|
2715
|
+
lat: new Date()
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
if (updateTemplate != null) {
|
|
2719
|
+
if (notificationSummary != null) {
|
|
2720
|
+
await notificationSummaryDocument.update(updateTemplate);
|
|
2721
|
+
updated = true;
|
|
2722
|
+
}
|
|
2723
|
+
else if (allowCreateNotificationSummaries) {
|
|
2724
|
+
// if it does not exist, and we are allowed to create new summaries, create it and add the new notifications
|
|
2725
|
+
const createTemplate = {
|
|
2726
|
+
...makeNewNotificationSummaryTemplate(firebase.inferKeyFromTwoWayFlatFirestoreModelKey(notificationSummaryId)),
|
|
2727
|
+
...updateTemplate
|
|
2728
|
+
};
|
|
2729
|
+
await notificationSummaryDocument.create(createTemplate);
|
|
2730
|
+
updated = true;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
return updated;
|
|
2734
|
+
})
|
|
2735
|
+
.then((updated) => {
|
|
2736
|
+
if (updated) {
|
|
2737
|
+
success.push(notificationSummaryId);
|
|
2738
|
+
}
|
|
2739
|
+
else {
|
|
2740
|
+
ignored.push(notificationSummaryId);
|
|
2741
|
+
}
|
|
2742
|
+
})
|
|
2743
|
+
.catch((e) => {
|
|
2744
|
+
console.error('firestoreNotificationSummarySendService(): failed updating notification summary', e);
|
|
2745
|
+
failed.push(notificationSummaryId);
|
|
2746
|
+
});
|
|
2747
|
+
});
|
|
2748
|
+
const sendResult = {
|
|
2749
|
+
success,
|
|
2750
|
+
failed,
|
|
2751
|
+
ignored
|
|
2752
|
+
};
|
|
2753
|
+
return sendResult;
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
return sendService;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/**
|
|
2761
|
+
* NotificationTextSendService that ignores sending all messages.
|
|
2762
|
+
*
|
|
2763
|
+
* This is useful for cases where your app may eventually want to send text notifications and want the rest of your app configured like it currently does.
|
|
2764
|
+
*
|
|
2765
|
+
* @returns
|
|
2766
|
+
*/
|
|
2767
|
+
function ignoreSendNotificationTextSendService() {
|
|
2768
|
+
const sendService = {
|
|
2769
|
+
async buildSendInstanceForTextNotificationMessages(notificationMessages) {
|
|
2770
|
+
return async () => {
|
|
2771
|
+
const success = [];
|
|
2772
|
+
const failed = [];
|
|
2773
|
+
const ignored = notificationMessages.map((x) => x.inputContext.recipient.t);
|
|
2774
|
+
const sendResult = {
|
|
2775
|
+
success,
|
|
2776
|
+
failed,
|
|
2777
|
+
ignored
|
|
2778
|
+
};
|
|
2779
|
+
return sendResult;
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
return sendService;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/**
|
|
2787
|
+
* A basic NotificationTaskService implementation.
|
|
2788
|
+
*/
|
|
2789
|
+
function notificationTaskService(config) {
|
|
2790
|
+
const { handlers: inputHandlers } = config;
|
|
2791
|
+
const handlers = {};
|
|
2792
|
+
inputHandlers.forEach((handlerConfig) => {
|
|
2793
|
+
const { type } = handlerConfig;
|
|
2794
|
+
handlers[type] = handlerForConfig(handlerConfig);
|
|
2795
|
+
});
|
|
2796
|
+
function handlerForConfig(handlerConfig) {
|
|
2797
|
+
const { flow: inputFlows, allowRunMultipleParts } = handlerConfig;
|
|
2798
|
+
const { included: checkpointFlows, excluded: nonCheckpointFlows } = util.separateValues(inputFlows, (x) => x.checkpoint != null);
|
|
2799
|
+
if (inputFlows.length === 0) {
|
|
2800
|
+
throw new Error('notificationTaskService(): NotificationTaskServiceTaskHandlerConfig must have at least one flow entry.');
|
|
2801
|
+
}
|
|
2802
|
+
else if (nonCheckpointFlows.length > 1) {
|
|
2803
|
+
throw new Error('notificationTaskService(): NotificationTaskServiceTaskHandlerConfig must not have more than one non-checkpoint flow.');
|
|
2804
|
+
}
|
|
2805
|
+
return {
|
|
2806
|
+
handleNotificationTask: async (notificationTask) => {
|
|
2807
|
+
const { checkpoints: completedCheckpoints } = notificationTask;
|
|
2808
|
+
let fn;
|
|
2809
|
+
switch (completedCheckpoints.length) {
|
|
2810
|
+
case 0:
|
|
2811
|
+
fn = (nonCheckpointFlows[0] ?? checkpointFlows[0])?.fn;
|
|
2812
|
+
break;
|
|
2813
|
+
default:
|
|
2814
|
+
const completedCheckpointsSet = new Set(completedCheckpoints);
|
|
2815
|
+
/**
|
|
2816
|
+
* Find the next flow function that hasn't had its checkpoint completed yet.
|
|
2817
|
+
*/
|
|
2818
|
+
const nextCheckpoint = checkpointFlows.find((x) => !completedCheckpointsSet.has(x.checkpoint));
|
|
2819
|
+
fn = nextCheckpoint?.fn;
|
|
2820
|
+
break;
|
|
2821
|
+
}
|
|
2822
|
+
let result;
|
|
2823
|
+
if (fn) {
|
|
2824
|
+
result = await fn(notificationTask);
|
|
2825
|
+
// if allowRunMultipleParts is true, and the result doesn't have a canRunNextCheckpoint value, then set it to true.
|
|
2826
|
+
if (allowRunMultipleParts && result.canRunNextCheckpoint == null) {
|
|
2827
|
+
result.canRunNextCheckpoint = true;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
else {
|
|
2831
|
+
result = {
|
|
2832
|
+
completion: true // if there are no functions remaining, then the task is complete
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
return result;
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
return {
|
|
2840
|
+
isKnownNotificationTaskType: (notificationTaskType) => {
|
|
2841
|
+
return handlers[notificationTaskType] !== undefined;
|
|
2842
|
+
},
|
|
2843
|
+
taskHandlerForNotificationTaskType: (notificationTaskType) => handlers[notificationTaskType]
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
/**
|
|
2848
|
+
* Creates a NotificationTaskSubtaskNotificationTaskHandlerFactory.
|
|
2849
|
+
*/
|
|
2850
|
+
function notificationTaskSubtaskNotificationTaskHandlerFactory(factoryConfig) {
|
|
2851
|
+
const { taskType, subtaskHandlerFunctionName: subtaskHandlerName, inputFunction, defaultCleanup, cleanupFunction, buildUpdateMetadata: inputBuildUpdateMetadata } = factoryConfig;
|
|
2852
|
+
return (subtaskHandlerConfig) => {
|
|
2853
|
+
const { processors: inputProcessors, maxCleanupRetryAttempts: inputMaxCleanupRetryAttempts, cleanupRetryDelay: inputCleanupRetryDelay, defaultAllowRunMultipleParts } = subtaskHandlerConfig;
|
|
2854
|
+
const maxCleanupRetryAttempts = inputMaxCleanupRetryAttempts ?? firebase.DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_ATTEMPTS;
|
|
2855
|
+
const cleanupRetryDelay = inputCleanupRetryDelay ?? firebase.DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_DELAY;
|
|
2856
|
+
const buildUpdateMetadata = inputBuildUpdateMetadata ?? (() => undefined);
|
|
2857
|
+
const processors = {};
|
|
2858
|
+
inputProcessors.forEach((processorConfig) => {
|
|
2859
|
+
const { target } = processorConfig;
|
|
2860
|
+
processors[target] = processorFunctionForConfig(processorConfig);
|
|
2861
|
+
});
|
|
2862
|
+
/**
|
|
2863
|
+
* Structure is similar to notificationTaskService(), but contained to handle the subtasks.
|
|
2864
|
+
*/
|
|
2865
|
+
function processorFunctionForConfig(processorConfig) {
|
|
2866
|
+
const { flow: inputFlows, cleanup, allowRunMultipleParts: processorAllowRunMultipleParts } = processorConfig;
|
|
2867
|
+
const { included: subtaskFlows, excluded: nonSubtaskFlows } = util.separateValues(inputFlows, (x) => x.subtask != null);
|
|
2868
|
+
const allowRunMultipleParts = processorAllowRunMultipleParts ?? defaultAllowRunMultipleParts;
|
|
2869
|
+
if (inputFlows.length === 0) {
|
|
2870
|
+
throw new Error(`${subtaskHandlerName}(): NotificationTaskSubtaskProcessorConfig must have at least one flow entry.`);
|
|
2871
|
+
}
|
|
2872
|
+
else if (nonSubtaskFlows.length > 1) {
|
|
2873
|
+
throw new Error(`${subtaskHandlerName}(): NotificationTaskSubtaskProcessorConfig must not have more than one non-subtask flow.`);
|
|
2874
|
+
}
|
|
2875
|
+
const allKnownSubtasks = util.unique(inputFlows.map((x) => x.subtask));
|
|
2876
|
+
return {
|
|
2877
|
+
process: async (input) => {
|
|
2878
|
+
const { notificationTask, completedSubtasks, subtaskData } = input;
|
|
2879
|
+
let fn;
|
|
2880
|
+
switch (completedSubtasks.length) {
|
|
2881
|
+
case 0:
|
|
2882
|
+
fn = (nonSubtaskFlows[0] ?? subtaskFlows[0])?.fn;
|
|
2883
|
+
break;
|
|
2884
|
+
default:
|
|
2885
|
+
const completedSubtasksSet = new Set(completedSubtasks);
|
|
2886
|
+
/**
|
|
2887
|
+
* Find the next flow function that hasn't had its checkpoint completed yet.
|
|
2888
|
+
*/
|
|
2889
|
+
const nextSubtask = subtaskFlows.find((x) => !completedSubtasksSet.has(x.subtask));
|
|
2890
|
+
fn = nextSubtask?.fn;
|
|
2891
|
+
break;
|
|
2892
|
+
}
|
|
2893
|
+
let result;
|
|
2894
|
+
if (fn) {
|
|
2895
|
+
/*
|
|
2896
|
+
* This section is similar to handleNotificationTask() in notification.action.server.ts,
|
|
2897
|
+
* but is modified to handle the subtasks. The main difference is the attempt count is maintained,
|
|
2898
|
+
* and instead is available via the normal NotificationTask attempts details.
|
|
2899
|
+
*/
|
|
2900
|
+
const subtaskResult = await fn(input);
|
|
2901
|
+
const { completion: subtaskCompletion, updateMetadata: subtaskUpdateMetadata, delayUntil, canRunNextCheckpoint } = subtaskResult;
|
|
2902
|
+
let allSubtasksDone = false;
|
|
2903
|
+
let sfps = completedSubtasks;
|
|
2904
|
+
// update the task metadata to reflect the changes
|
|
2905
|
+
switch (subtaskCompletion) {
|
|
2906
|
+
case true:
|
|
2907
|
+
allSubtasksDone = true;
|
|
2908
|
+
break;
|
|
2909
|
+
case false:
|
|
2910
|
+
// remove any completions, if applicable
|
|
2911
|
+
sfps = removeFromCompletionsArrayWithTaskResult(sfps, subtaskResult);
|
|
2912
|
+
break;
|
|
2913
|
+
default:
|
|
2914
|
+
sfps = util.unique([
|
|
2915
|
+
...removeFromCompletionsArrayWithTaskResult(sfps, subtaskResult), // remove any completions, if applicable
|
|
2916
|
+
...util.asArray(subtaskCompletion)
|
|
2917
|
+
]);
|
|
2918
|
+
const completedSubtasksSet = new Set(sfps);
|
|
2919
|
+
const incompleteSubtasks = allKnownSubtasks.filter((x) => !completedSubtasksSet.has(x));
|
|
2920
|
+
allSubtasksDone = incompleteSubtasks.length === 0;
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
// configure the update metadata result
|
|
2924
|
+
const sd = {
|
|
2925
|
+
...subtaskData,
|
|
2926
|
+
...subtaskUpdateMetadata
|
|
2927
|
+
};
|
|
2928
|
+
/**
|
|
2929
|
+
* This is updating the metadata for the NotificationTask, which has a nested data
|
|
2930
|
+
*/
|
|
2931
|
+
const baseUpdateMetadata = {
|
|
2932
|
+
...notificationTask.data,
|
|
2933
|
+
sfps,
|
|
2934
|
+
sd
|
|
2935
|
+
};
|
|
2936
|
+
let updateMetadata = (await buildUpdateMetadata(baseUpdateMetadata, input));
|
|
2937
|
+
if (updateMetadata) {
|
|
2938
|
+
// inject sfps and sd back in
|
|
2939
|
+
updateMetadata = {
|
|
2940
|
+
...updateMetadata,
|
|
2941
|
+
sfps,
|
|
2942
|
+
sd
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
else {
|
|
2946
|
+
updateMetadata = baseUpdateMetadata;
|
|
2947
|
+
}
|
|
2948
|
+
const nextCanRunNextCheckpoint = canRunNextCheckpoint ?? allowRunMultipleParts;
|
|
2949
|
+
result = {
|
|
2950
|
+
completion: allSubtasksDone ? ['processing'] : firebase.delayCompletion(), // return processing until all subtasks are complete.
|
|
2951
|
+
updateMetadata,
|
|
2952
|
+
canRunNextCheckpoint: nextCanRunNextCheckpoint,
|
|
2953
|
+
allCompletedSubTasks: sfps,
|
|
2954
|
+
delayUntil // delay is passed through
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
else {
|
|
2958
|
+
// no more subtasks to process, and no metadata changes. Mark as processing complete and continue.
|
|
2959
|
+
result = firebase.completeSubtaskProcessingAndScheduleCleanupTaskResult();
|
|
2960
|
+
}
|
|
2961
|
+
return result;
|
|
2962
|
+
},
|
|
2963
|
+
cleanup
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function useInputDataFactory(fn) {
|
|
2967
|
+
return async (notificationTask) => {
|
|
2968
|
+
const { data } = notificationTask;
|
|
2969
|
+
let result;
|
|
2970
|
+
if (data) {
|
|
2971
|
+
try {
|
|
2972
|
+
const inputFunctionResult = await inputFunction(data, notificationTask);
|
|
2973
|
+
result = await fn(notificationTask, inputFunctionResult, data);
|
|
2974
|
+
}
|
|
2975
|
+
catch (e) {
|
|
2976
|
+
if (e instanceof NotificationTaskSubTaskMissingRequiredDataTermination) {
|
|
2977
|
+
// Task is complete if the required data no longer exists. Nothing to cleanup.
|
|
2978
|
+
result = firebase.notificationTaskComplete();
|
|
2979
|
+
}
|
|
2980
|
+
else {
|
|
2981
|
+
// rethrow the error
|
|
2982
|
+
throw e;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
else {
|
|
2987
|
+
// Improperly configured task. Complete immediately.
|
|
2988
|
+
result = firebase.notificationTaskComplete();
|
|
2989
|
+
}
|
|
2990
|
+
return result;
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
const result = {
|
|
2994
|
+
type: taskType,
|
|
2995
|
+
flow: [
|
|
2996
|
+
{
|
|
2997
|
+
checkpoint: firebase.NOTIFICATION_TASK_SUBTASK_CHECKPOINT_PROCESSING,
|
|
2998
|
+
fn: useInputDataFactory(async (notificationTask, inputFunctionResult, data) => {
|
|
2999
|
+
let result;
|
|
3000
|
+
const baseInput = {
|
|
3001
|
+
...inputFunctionResult,
|
|
3002
|
+
notificationTask
|
|
3003
|
+
};
|
|
3004
|
+
const { target } = baseInput;
|
|
3005
|
+
if (target) {
|
|
3006
|
+
const processor = processors[target];
|
|
3007
|
+
if (processor) {
|
|
3008
|
+
const { sd: subtaskData, sfps: completedSubtasks } = data;
|
|
3009
|
+
const input = {
|
|
3010
|
+
...baseInput,
|
|
3011
|
+
target,
|
|
3012
|
+
completedSubtasks: completedSubtasks ?? [],
|
|
3013
|
+
subtaskData
|
|
3014
|
+
};
|
|
3015
|
+
result = await processor.process(input);
|
|
3016
|
+
}
|
|
3017
|
+
else {
|
|
3018
|
+
// processor is unknown. Complete the task.
|
|
3019
|
+
result = firebase.completeSubtaskProcessingAndScheduleCleanupTaskResult();
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
else {
|
|
3023
|
+
// target is unknown. Complete the task.
|
|
3024
|
+
result = firebase.completeSubtaskProcessingAndScheduleCleanupTaskResult();
|
|
3025
|
+
}
|
|
3026
|
+
return result;
|
|
3027
|
+
})
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
checkpoint: firebase.NOTIFICATION_TASK_SUBTASK_CHECKPOINT_CLEANUP,
|
|
3031
|
+
fn: useInputDataFactory(async (notificationTask, inputFunctionResult, data) => {
|
|
3032
|
+
let result;
|
|
3033
|
+
let cleanupFunctionInput = {
|
|
3034
|
+
...inputFunctionResult,
|
|
3035
|
+
notificationTask
|
|
3036
|
+
};
|
|
3037
|
+
const { target } = cleanupFunctionInput;
|
|
3038
|
+
let cleanupInstructions;
|
|
3039
|
+
if (target) {
|
|
3040
|
+
const processor = processors[target];
|
|
3041
|
+
if (processor && processor.cleanup) {
|
|
3042
|
+
const { sd: subtaskData, sfps: completedSubtasks } = data;
|
|
3043
|
+
const input = {
|
|
3044
|
+
...cleanupFunctionInput,
|
|
3045
|
+
notificationTask,
|
|
3046
|
+
completedSubtasks: completedSubtasks ?? [],
|
|
3047
|
+
target,
|
|
3048
|
+
subtaskData
|
|
3049
|
+
};
|
|
3050
|
+
cleanupInstructions = (await processor.cleanup(input, defaultCleanup)) ?? (await defaultCleanup(cleanupFunctionInput));
|
|
3051
|
+
cleanupFunctionInput = input;
|
|
3052
|
+
}
|
|
3053
|
+
else {
|
|
3054
|
+
// processor is unknown. Complete the task.
|
|
3055
|
+
cleanupInstructions = await defaultCleanup(cleanupFunctionInput);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
else {
|
|
3059
|
+
// target is unknown. Complete the task.
|
|
3060
|
+
cleanupInstructions = await defaultCleanup(cleanupFunctionInput);
|
|
3061
|
+
}
|
|
3062
|
+
if (cleanupInstructions.cleanupSuccess === false && notificationTask.currentCheckpointSendAttempts <= maxCleanupRetryAttempts) {
|
|
3063
|
+
result = firebase.notificationTaskDelayRetry(cleanupInstructions.delayRetryUntil ?? cleanupRetryDelay);
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
result = await cleanupFunction(cleanupFunctionInput, cleanupInstructions);
|
|
3067
|
+
}
|
|
3068
|
+
return result;
|
|
3069
|
+
})
|
|
3070
|
+
}
|
|
3071
|
+
]
|
|
3072
|
+
};
|
|
3073
|
+
return result;
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
// MARK: Internally Handled Errors
|
|
3077
|
+
/**
|
|
3078
|
+
* Thrown when a subtask no longer has data available to continue processing.
|
|
3079
|
+
*
|
|
3080
|
+
* The subtask will be marked as immediately complete, and no cleanup will occur.
|
|
3081
|
+
*
|
|
3082
|
+
* This is useful in cases where the underlying models or data that the subtask rely on are deleted (and those models were also required for cleanup) so the task can be marked as complete without attempting cleanup.
|
|
3083
|
+
*/
|
|
3084
|
+
class NotificationTaskSubTaskMissingRequiredDataTermination extends makeError.BaseError {
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Creates a NotificationTaskSubTaskMissingRequiredDataTermination.
|
|
3088
|
+
*/
|
|
3089
|
+
function notificationTaskSubTaskMissingRequiredDataTermination() {
|
|
3090
|
+
return new NotificationTaskSubTaskMissingRequiredDataTermination();
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
function storageFileModelAlreadyInitializedError() {
|
|
3094
|
+
return firebaseServer.preconditionConflictError({
|
|
3095
|
+
message: `This model has already been initialized.`,
|
|
3096
|
+
code: firebase.STORAGE_FILE_MODEL_ALREADY_INITIALIZED_ERROR_CODE
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
function storageFileNotFlaggedForGroupsSyncError() {
|
|
3100
|
+
return firebaseServer.preconditionConflictError({
|
|
3101
|
+
message: `This StorageFile has not been flagged for sync with its groups.`,
|
|
3102
|
+
code: firebase.STORAGE_FILE_NOT_FLAGGED_FOR_GROUPS_SYNC_ERROR_CODE
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
function uploadedFileDoesNotExistError() {
|
|
3106
|
+
return firebaseServer.preconditionConflictError({
|
|
3107
|
+
message: `The target uploaded file does not exist.`,
|
|
3108
|
+
code: firebase.UPLOADED_FILE_DOES_NOT_EXIST_ERROR_CODE
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
function uploadedFileIsNotAllowedToBeInitializedError() {
|
|
3112
|
+
return firebaseServer.preconditionConflictError({
|
|
3113
|
+
message: `The target uploaded file is not allowed to be initialized.`,
|
|
3114
|
+
code: firebase.UPLOADED_FILE_NOT_ALLOWED_TO_BE_INITIALIZED_ERROR_CODE
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
function uploadedFileInitializationFailedError(data) {
|
|
3118
|
+
return firebaseServer.internalServerError({
|
|
3119
|
+
message: `The target uploaded file initialization failed with result type "${data.resultType}".`,
|
|
3120
|
+
code: firebase.UPLOADED_FILE_INITIALIZATION_FAILED_ERROR_CODE,
|
|
3121
|
+
data: {
|
|
3122
|
+
resultType: data.resultType
|
|
3123
|
+
}
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
function uploadedFileInitializationDiscardedError() {
|
|
3127
|
+
return firebaseServer.internalServerError({
|
|
3128
|
+
message: `The target uploaded file initialization was discarded.`,
|
|
3129
|
+
code: firebase.UPLOADED_FILE_INITIALIZATION_DISCARDED_ERROR_CODE
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
function storageFileProcessingNotAllowedForInvalidStateError() {
|
|
3133
|
+
return firebaseServer.preconditionConflictError({
|
|
3134
|
+
message: `The target StorageFileDocument must be in an OK state to be processed and processing not flagged as SHOULD_NOT_PROCESS.`,
|
|
3135
|
+
code: firebase.STORAGE_FILE_PROCESSING_NOT_ALLOWED_FOR_INVALID_STATE_ERROR_CODE
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
function storageFileProcessingNotQueuedForProcessingError() {
|
|
3139
|
+
return firebaseServer.preconditionConflictError({
|
|
3140
|
+
message: `The target StorageFileDocument is not queued for processing.`,
|
|
3141
|
+
code: firebase.STORAGE_FILE_PROCESSING_NOT_QUEUED_FOR_PROCESSING_ERROR_CODE
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
function storageFileProcessingNotAvailableForTypeError() {
|
|
3145
|
+
return firebaseServer.preconditionConflictError({
|
|
3146
|
+
message: `The target StorageFileDocument is not available for processing.`,
|
|
3147
|
+
code: firebase.STORAGE_FILE_PROCESSING_NOT_AVAILABLE_FOR_TYPE_ERROR_CODE
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
function storageFileAlreadyProcessedError() {
|
|
3151
|
+
return firebaseServer.preconditionConflictError({
|
|
3152
|
+
message: `The target StorageFileDocument has already finished processing.`,
|
|
3153
|
+
code: firebase.STORAGE_FILE_ALREADY_PROCESSED_ERROR_CODE
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
function storageFileNotFlaggedForDeletionError() {
|
|
3157
|
+
return firebaseServer.preconditionConflictError({
|
|
3158
|
+
message: `The target StorageFileDocument is not flagged for deletion.`,
|
|
3159
|
+
code: firebase.STORAGE_FILE_NOT_FLAGGED_FOR_DELETION_ERROR_CODE
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
function storageFileCannotBeDeletedYetError() {
|
|
3163
|
+
return firebaseServer.preconditionConflictError({
|
|
3164
|
+
message: `The target StorageFileDocument cannot be deleted yet.`,
|
|
3165
|
+
code: firebase.STORAGE_FILE_CANNOT_BE_DELETED_YET_ERROR_CODE
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
function storageFileGroupQueuedForInitializationError() {
|
|
3169
|
+
return firebaseServer.preconditionConflictError({
|
|
3170
|
+
message: `The target StorageFileGroupDocument is queued for initialization.`,
|
|
3171
|
+
code: firebase.STORAGE_FILE_GROUP_QUEUED_FOR_INITIALIZATION_ERROR_CODE
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
function createStorageFileGroupInputError() {
|
|
3175
|
+
return firebaseServer.preconditionConflictError({
|
|
3176
|
+
message: `The model or storageFileId is required for creating a StorageFileGroup.`,
|
|
3177
|
+
code: firebase.STORAGE_FILE_GROUP_CREATE_INPUT_ERROR_CODE
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
/**
|
|
3182
|
+
* Injection token for the BaseStorageFileServerActionsContext
|
|
3183
|
+
*/
|
|
3184
|
+
const BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = 'BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT';
|
|
3185
|
+
/**
|
|
3186
|
+
* Injection token for the StorageFileServerActionsContext
|
|
3187
|
+
*/
|
|
3188
|
+
const STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = 'STORAGE_FILE_SERVER_ACTION_CONTEXT';
|
|
3189
|
+
class StorageFileServerActions {
|
|
3190
|
+
}
|
|
3191
|
+
function storageFileServerActions(context) {
|
|
3192
|
+
return {
|
|
3193
|
+
createStorageFile: createStorageFileFactory(context),
|
|
3194
|
+
initializeAllStorageFilesFromUploads: initializeAllStorageFilesFromUploadsFactory(context),
|
|
3195
|
+
initializeStorageFileFromUpload: initializeStorageFileFromUploadFactory(context),
|
|
3196
|
+
updateStorageFile: updateStorageFileFactory(context),
|
|
3197
|
+
processAllQueuedStorageFiles: processAllQueuedStorageFilesFactory(context),
|
|
3198
|
+
processStorageFile: processStorageFileFactory(context),
|
|
3199
|
+
deleteAllQueuedStorageFiles: deleteAllQueuedStorageFilesFactory(context),
|
|
3200
|
+
deleteStorageFile: deleteStorageFileFactory(context),
|
|
3201
|
+
downloadStorageFile: downloadStorageFileFactory(context),
|
|
3202
|
+
createStorageFileGroup: createStorageFileGroupFactory(context),
|
|
3203
|
+
updateStorageFileGroup: updateStorageFileGroupFactory(context),
|
|
3204
|
+
syncStorageFileWithGroups: syncStorageFileWithGroupsFactory(context),
|
|
3205
|
+
syncAllFlaggedStorageFilesWithGroups: syncAllFlaggedStorageFilesWithGroupsFactory(context),
|
|
3206
|
+
regenerateStorageFileGroupContent: regenerateStorageFileGroupContentFactory(context),
|
|
3207
|
+
regenerateAllFlaggedStorageFileGroupsContent: regenerateAllFlaggedStorageFileGroupsContentFactory(context)
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
// MARK: Actions
|
|
3211
|
+
function createStorageFileFactory(context) {
|
|
3212
|
+
const { storageFileCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
3213
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CreateStorageFileParams, async (params) => {
|
|
3214
|
+
return async () => {
|
|
3215
|
+
const storageFileDocument = null;
|
|
3216
|
+
// TODO: check the file exists, and pull the metadata, and create the document
|
|
3217
|
+
return storageFileDocument;
|
|
3218
|
+
};
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
function initializeAllStorageFilesFromUploadsFactory(context) {
|
|
3222
|
+
const { storageService, firebaseServerActionTransformFunctionFactory } = context;
|
|
3223
|
+
const _initializeStorageFileFromUploadFile = _initializeStorageFileFromUploadFileFactory(context);
|
|
3224
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeAllStorageFilesFromUploadsParams, async (params) => {
|
|
3225
|
+
const { folderPath, maxFilesToInitialize, overrideUploadsFolderPath } = params;
|
|
3226
|
+
const fullPath = util.mergeSlashPaths([overrideUploadsFolderPath ?? firebase.UPLOADS_FOLDER_PATH, folderPath]); // only targets the uploads folder
|
|
3227
|
+
return async () => {
|
|
3228
|
+
const folder = storageService.folder(fullPath);
|
|
3229
|
+
const modelKeys = [];
|
|
3230
|
+
let filesVisited = 0;
|
|
3231
|
+
let initializationsSuccessCount = 0;
|
|
3232
|
+
let initializationsFailureCount = 0;
|
|
3233
|
+
await firebase.iterateStorageListFilesByEachFile({
|
|
3234
|
+
folder,
|
|
3235
|
+
includeNestedResults: true,
|
|
3236
|
+
readItemsFromPageResult: (results) => results.result.files(),
|
|
3237
|
+
iterateEachPageItem: async (file) => {
|
|
3238
|
+
const fileInstance = file.file();
|
|
3239
|
+
const initializeResult = await _initializeStorageFileFromUploadFile({ file: fileInstance }).catch(() => null);
|
|
3240
|
+
filesVisited++;
|
|
3241
|
+
if (initializeResult) {
|
|
3242
|
+
initializationsSuccessCount++;
|
|
3243
|
+
modelKeys.push(initializeResult.key);
|
|
3244
|
+
}
|
|
3245
|
+
else {
|
|
3246
|
+
initializationsFailureCount++;
|
|
3247
|
+
}
|
|
3248
|
+
},
|
|
3249
|
+
/**
|
|
3250
|
+
* The maximum number of files to initialize at once.
|
|
3251
|
+
*/
|
|
3252
|
+
iterateItemsLimit: maxFilesToInitialize ?? 1000,
|
|
3253
|
+
/**
|
|
3254
|
+
* Iterate four separate pages at a time
|
|
3255
|
+
*/
|
|
3256
|
+
maxParallelPages: 4
|
|
3257
|
+
});
|
|
3258
|
+
const result = {
|
|
3259
|
+
modelKeys,
|
|
3260
|
+
filesVisited,
|
|
3261
|
+
initializationsSuccessCount,
|
|
3262
|
+
initializationsFailureCount
|
|
3263
|
+
};
|
|
3264
|
+
return result;
|
|
3265
|
+
};
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
function _initializeStorageFileFromUploadFileFactory(context) {
|
|
3269
|
+
const { firestoreContext, storageFileInitializeFromUploadService, notificationExpediteService } = context;
|
|
3270
|
+
const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
|
|
3271
|
+
return async (input) => {
|
|
3272
|
+
const { file, expediteProcessing } = input;
|
|
3273
|
+
const { bucketId, pathString } = file.storagePath;
|
|
3274
|
+
// file must exist
|
|
3275
|
+
const exists = await file.exists();
|
|
3276
|
+
if (!exists) {
|
|
3277
|
+
throw uploadedFileDoesNotExistError();
|
|
3278
|
+
}
|
|
3279
|
+
// file must be allowed to be initialized
|
|
3280
|
+
const isAllowedToBeInitialized = await storageFileInitializeFromUploadService.checkFileIsAllowedToBeInitialized(file);
|
|
3281
|
+
if (!isAllowedToBeInitialized) {
|
|
3282
|
+
throw uploadedFileIsNotAllowedToBeInitializedError();
|
|
3283
|
+
}
|
|
3284
|
+
let storageFileDocument;
|
|
3285
|
+
let initializationResult;
|
|
3286
|
+
let httpsError;
|
|
3287
|
+
try {
|
|
3288
|
+
initializationResult = await storageFileInitializeFromUploadService.initializeFromUpload({
|
|
3289
|
+
file
|
|
3290
|
+
});
|
|
3291
|
+
async function deleteFile() {
|
|
3292
|
+
try {
|
|
3293
|
+
// can now delete the uploaded file
|
|
3294
|
+
await file.delete();
|
|
3295
|
+
}
|
|
3296
|
+
catch (e) {
|
|
3297
|
+
// log errors here, but do nothing.
|
|
3298
|
+
console.error(`initializeStorageFileFromUpload(): Error deleting uploaded file (${bucketId}/${pathString})`, e);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
switch (initializationResult.resultType) {
|
|
3302
|
+
case 'success':
|
|
3303
|
+
await deleteFile();
|
|
3304
|
+
if (initializationResult.storageFileDocument) {
|
|
3305
|
+
storageFileDocument = initializationResult.storageFileDocument;
|
|
3306
|
+
// expedite processing if requested
|
|
3307
|
+
if (storageFileDocument != null && expediteProcessing) {
|
|
3308
|
+
const storageFile = await firebaseServer.assertSnapshotData(storageFileDocument);
|
|
3309
|
+
if (storageFile.ps === firebase.StorageFileProcessingState.QUEUED_FOR_PROCESSING) {
|
|
3310
|
+
const expediteInstance = notificationExpediteService.expediteInstance();
|
|
3311
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
3312
|
+
expediteInstance.initialize();
|
|
3313
|
+
await processStorageFileInTransaction({
|
|
3314
|
+
storageFileDocument: storageFileDocument,
|
|
3315
|
+
expediteInstance
|
|
3316
|
+
}, transaction);
|
|
3317
|
+
});
|
|
3318
|
+
await expediteInstance.send().catch(() => null);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
else {
|
|
3323
|
+
httpsError = uploadedFileInitializationDiscardedError();
|
|
3324
|
+
}
|
|
3325
|
+
break;
|
|
3326
|
+
case 'initializer_error':
|
|
3327
|
+
if (initializationResult.initializationError) {
|
|
3328
|
+
throw initializationResult.initializationError; // re-throw the encountered error
|
|
3329
|
+
}
|
|
3330
|
+
break;
|
|
3331
|
+
case 'permanent_initializer_failure':
|
|
3332
|
+
// log the error
|
|
3333
|
+
if (initializationResult.initializationError) {
|
|
3334
|
+
console.warn(`initializeStorageFileFromUpload(): Permanent initializer failure for file (${bucketId}/${pathString})`, initializationResult.initializationError);
|
|
3335
|
+
}
|
|
3336
|
+
// delete the file
|
|
3337
|
+
await deleteFile();
|
|
3338
|
+
// return the error
|
|
3339
|
+
httpsError = uploadedFileInitializationFailedError({
|
|
3340
|
+
resultType: initializationResult.resultType,
|
|
3341
|
+
fileDeleted: true
|
|
3342
|
+
});
|
|
3343
|
+
break;
|
|
3344
|
+
case 'no_determiner_match':
|
|
3345
|
+
case 'no_initializer_configured':
|
|
3346
|
+
default:
|
|
3347
|
+
httpsError = uploadedFileInitializationFailedError({
|
|
3348
|
+
resultType: initializationResult.resultType
|
|
3349
|
+
});
|
|
3350
|
+
console.error(`initializeStorageFileFromUpload(): Unknown file type (${initializationResult.resultType}) encountered for storage file "${bucketId}/${pathString}".`);
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
catch (e) {
|
|
3355
|
+
console.error(`initializeStorageFileFromUpload(): Error while initializing storage file (${bucketId}/${pathString}) from upload`, e);
|
|
3356
|
+
httpsError = uploadedFileInitializationFailedError({ resultType: 'initializer_error' });
|
|
3357
|
+
}
|
|
3358
|
+
if (httpsError) {
|
|
3359
|
+
throw httpsError;
|
|
3360
|
+
}
|
|
3361
|
+
else if (!storageFileDocument) {
|
|
3362
|
+
throw uploadedFileInitializationDiscardedError(); // throw again for redundancy
|
|
3363
|
+
}
|
|
3364
|
+
return storageFileDocument;
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
function initializeStorageFileFromUploadFactory(context) {
|
|
3368
|
+
const { storageService, firebaseServerActionTransformFunctionFactory } = context;
|
|
3369
|
+
const _initializeStorageFileFromUploadFile = _initializeStorageFileFromUploadFileFactory(context);
|
|
3370
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeStorageFileFromUploadParams, async (params) => {
|
|
3371
|
+
const { bucketId, pathString, expediteProcessing } = params;
|
|
3372
|
+
return async () => {
|
|
3373
|
+
const file = storageService.file(bucketId == null ? pathString : { bucketId, pathString });
|
|
3374
|
+
return _initializeStorageFileFromUploadFile({ file, expediteProcessing });
|
|
3375
|
+
};
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
function updateStorageFileFactory(context) {
|
|
3379
|
+
const { storageFileCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
3380
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateStorageFileParams, async (params) => {
|
|
3381
|
+
const { sdat } = params;
|
|
3382
|
+
return async (storageFileDocument) => {
|
|
3383
|
+
const updateTemplate = {
|
|
3384
|
+
sdat
|
|
3385
|
+
};
|
|
3386
|
+
await storageFileDocument.update(updateTemplate);
|
|
3387
|
+
return storageFileDocument;
|
|
3388
|
+
};
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
function updateStorageFileGroupFactory(context) {
|
|
3392
|
+
const { firestoreContext, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3393
|
+
return firebaseServerActionTransformFunctionFactory(firebase.UpdateStorageFileGroupParams, async (params) => {
|
|
3394
|
+
const { entries } = params;
|
|
3395
|
+
return async (storageFileGroupDocument) => {
|
|
3396
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
3397
|
+
const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
|
|
3398
|
+
const storageFileGroup = await firebaseServer.assertSnapshotData(storageFileGroupDocumentInTransaction);
|
|
3399
|
+
let f = undefined;
|
|
3400
|
+
// update entries
|
|
3401
|
+
if (entries?.length) {
|
|
3402
|
+
f = util.ModelRelationUtility.updateCollection(storageFileGroup.f, entries, {
|
|
3403
|
+
readKey: (x) => x.s,
|
|
3404
|
+
merge: (existing, update) => {
|
|
3405
|
+
const n = update.n === undefined ? existing.n : update.n;
|
|
3406
|
+
return {
|
|
3407
|
+
...existing,
|
|
3408
|
+
n
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
const updateTemplate = {
|
|
3414
|
+
f
|
|
3415
|
+
};
|
|
3416
|
+
await storageFileGroupDocumentInTransaction.update(updateTemplate);
|
|
3417
|
+
});
|
|
3418
|
+
return storageFileGroupDocument;
|
|
3419
|
+
};
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
function processAllQueuedStorageFilesFactory(context) {
|
|
3423
|
+
const { storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3424
|
+
const processStorageFile = processStorageFileFactory(context);
|
|
3425
|
+
return firebaseServerActionTransformFunctionFactory(firebase.ProcessAllQueuedStorageFilesParams, async (params) => {
|
|
3426
|
+
return async () => {
|
|
3427
|
+
let storageFilesVisited = 0;
|
|
3428
|
+
let storageFilesProcessStarted = 0;
|
|
3429
|
+
let storageFilesFailedStarting = 0;
|
|
3430
|
+
const proceessStorageFileParams = {
|
|
3431
|
+
key: firebase.firestoreDummyKey()
|
|
3432
|
+
};
|
|
3433
|
+
const processStorageFileInstance = await processStorageFile(proceessStorageFileParams);
|
|
3434
|
+
await firebase.iterateFirestoreDocumentSnapshotPairs({
|
|
3435
|
+
documentAccessor: storageFileCollection.documentAccessor(),
|
|
3436
|
+
iterateSnapshotPair: async (snapshotPair) => {
|
|
3437
|
+
storageFilesVisited++;
|
|
3438
|
+
const processStorageFileResult = await processStorageFileInstance(snapshotPair.document).catch(() => null);
|
|
3439
|
+
if (processStorageFileResult) {
|
|
3440
|
+
storageFilesProcessStarted++;
|
|
3441
|
+
}
|
|
3442
|
+
else {
|
|
3443
|
+
storageFilesFailedStarting++;
|
|
3444
|
+
}
|
|
3445
|
+
},
|
|
3446
|
+
constraintsFactory: () => firebase.storageFilesQueuedForProcessingQuery(),
|
|
3447
|
+
queryFactory: storageFileCollection,
|
|
3448
|
+
batchSize: undefined,
|
|
3449
|
+
performTasksConfig: {
|
|
3450
|
+
maxParallelTasks: 10
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
const result = {
|
|
3454
|
+
storageFilesVisited,
|
|
3455
|
+
storageFilesProcessStarted,
|
|
3456
|
+
storageFilesFailedStarting
|
|
3457
|
+
};
|
|
3458
|
+
return result;
|
|
3459
|
+
};
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
function _processStorageFileInTransactionFactory(context) {
|
|
3463
|
+
const { storageFileCollection, notificationCollectionGroup } = context;
|
|
3464
|
+
return async (input, transaction) => {
|
|
3465
|
+
const { storageFileDocument, storageFile: inputStorageFile, params, expediteInstance } = input;
|
|
3466
|
+
const { checkRetryProcessing, forceRestartProcessing, processAgainIfSuccessful } = params ?? {};
|
|
3467
|
+
const storageFileDocumentInTransaction = storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileDocument);
|
|
3468
|
+
const storageFile = inputStorageFile ?? (await firebaseServer.assertSnapshotData(storageFileDocumentInTransaction));
|
|
3469
|
+
async function beginProcessing(overrideExistingTask) {
|
|
3470
|
+
const state = storageFile.fs;
|
|
3471
|
+
// check the storageFile is in the OK state
|
|
3472
|
+
if (state !== firebase.StorageFileState.OK) {
|
|
3473
|
+
throw storageFileProcessingNotAllowedForInvalidStateError();
|
|
3474
|
+
}
|
|
3475
|
+
const createNotificationTaskResult = await firebase.createNotificationDocument({
|
|
3476
|
+
context,
|
|
3477
|
+
transaction,
|
|
3478
|
+
template: firebase.storageFileProcessingNotificationTaskTemplate({
|
|
3479
|
+
storageFileDocument,
|
|
3480
|
+
overrideExistingTask
|
|
3481
|
+
})
|
|
3482
|
+
});
|
|
3483
|
+
await storageFileDocumentInTransaction.update({
|
|
3484
|
+
ps: firebase.StorageFileProcessingState.PROCESSING,
|
|
3485
|
+
pat: new Date(), // set new processing start date
|
|
3486
|
+
pcat: null, // clear processing completion date
|
|
3487
|
+
pn: createNotificationTaskResult.notificationDocument.key
|
|
3488
|
+
});
|
|
3489
|
+
expediteInstance?.enqueueCreateResult(createNotificationTaskResult);
|
|
3490
|
+
}
|
|
3491
|
+
switch (storageFile.ps) {
|
|
3492
|
+
case firebase.StorageFileProcessingState.INIT_OR_NONE:
|
|
3493
|
+
// queue up for processing, unless it has no purpose
|
|
3494
|
+
if (!storageFile.p) {
|
|
3495
|
+
throw storageFileProcessingNotAvailableForTypeError();
|
|
3496
|
+
}
|
|
3497
|
+
else {
|
|
3498
|
+
await beginProcessing(false);
|
|
3499
|
+
}
|
|
3500
|
+
break;
|
|
3501
|
+
case firebase.StorageFileProcessingState.QUEUED_FOR_PROCESSING:
|
|
3502
|
+
// begin processing
|
|
3503
|
+
await beginProcessing(false);
|
|
3504
|
+
break;
|
|
3505
|
+
case firebase.StorageFileProcessingState.PROCESSING:
|
|
3506
|
+
// check if the processing task is still running
|
|
3507
|
+
const shouldCheckProcessing = !util.isThrottled(firebase.STORAGE_FILE_PROCESSING_STUCK_THROTTLE_CHECK_MS, storageFile.pat);
|
|
3508
|
+
if (!storageFile.pn) {
|
|
3509
|
+
await beginProcessing(true); // if no processing task is set, restart processing to recover from the broken state
|
|
3510
|
+
}
|
|
3511
|
+
else {
|
|
3512
|
+
const { pn } = storageFile;
|
|
3513
|
+
const notificationDocument = notificationCollectionGroup.documentAccessorForTransaction(transaction).loadDocumentForKey(pn);
|
|
3514
|
+
if (checkRetryProcessing || shouldCheckProcessing) {
|
|
3515
|
+
const notification = await notificationDocument.snapshotData();
|
|
3516
|
+
if (!notification) {
|
|
3517
|
+
// the notification document is missing. Re-begin processing
|
|
3518
|
+
await beginProcessing(true);
|
|
3519
|
+
}
|
|
3520
|
+
else if (notification.d || forceRestartProcessing) {
|
|
3521
|
+
// if the notification is somehow in the done state but the StorageFile never got notified in the same transaction, requeue.
|
|
3522
|
+
await beginProcessing(true);
|
|
3523
|
+
}
|
|
3524
|
+
// NOTE: We could look at the state of the notification task more, but at this point the task is probably still valid and still running,
|
|
3525
|
+
// so we can only wait on it. In general if the task still exists and is not yet done, then we should wait on it as the
|
|
3526
|
+
// task running system should complete eventually by design.
|
|
3527
|
+
}
|
|
3528
|
+
else if (expediteInstance) {
|
|
3529
|
+
// enqueue the existing notification to be run in the expedite instance
|
|
3530
|
+
expediteInstance.enqueue(notificationDocument);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
break;
|
|
3534
|
+
case firebase.StorageFileProcessingState.DO_NOT_PROCESS:
|
|
3535
|
+
throw storageFileProcessingNotQueuedForProcessingError();
|
|
3536
|
+
case firebase.StorageFileProcessingState.SUCCESS:
|
|
3537
|
+
if (forceRestartProcessing || processAgainIfSuccessful) {
|
|
3538
|
+
await beginProcessing(true);
|
|
3539
|
+
}
|
|
3540
|
+
else {
|
|
3541
|
+
throw storageFileAlreadyProcessedError();
|
|
3542
|
+
}
|
|
3543
|
+
break;
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
function processStorageFileFactory(context) {
|
|
3548
|
+
const { firestoreContext, notificationExpediteService, firebaseServerActionTransformFunctionFactory } = context;
|
|
3549
|
+
const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
|
|
3550
|
+
return firebaseServerActionTransformFunctionFactory(firebase.ProcessStorageFileParams, async (params) => {
|
|
3551
|
+
const { runImmediately } = params;
|
|
3552
|
+
return async (storageFileDocument) => {
|
|
3553
|
+
const expediteInstance = notificationExpediteService.expediteInstance();
|
|
3554
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
3555
|
+
expediteInstance.initialize();
|
|
3556
|
+
await processStorageFileInTransaction({
|
|
3557
|
+
storageFileDocument,
|
|
3558
|
+
params,
|
|
3559
|
+
expediteInstance
|
|
3560
|
+
}, transaction);
|
|
3561
|
+
});
|
|
3562
|
+
let expediteResult = null;
|
|
3563
|
+
// expedite the task if requested
|
|
3564
|
+
if (runImmediately) {
|
|
3565
|
+
expediteResult = await expediteInstance.send().then((x) => x[0]);
|
|
3566
|
+
}
|
|
3567
|
+
const result = {
|
|
3568
|
+
runImmediately: runImmediately ?? false,
|
|
3569
|
+
expediteResult
|
|
3570
|
+
};
|
|
3571
|
+
return result;
|
|
3572
|
+
};
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
function deleteAllQueuedStorageFilesFactory(context) {
|
|
3576
|
+
const { storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3577
|
+
const deleteStorageFile = deleteStorageFileFactory(context);
|
|
3578
|
+
return firebaseServerActionTransformFunctionFactory(firebase.DeleteAllQueuedStorageFilesParams, async (params) => {
|
|
3579
|
+
return async () => {
|
|
3580
|
+
let storageFilesVisited = 0;
|
|
3581
|
+
let storageFilesDeleted = 0;
|
|
3582
|
+
let storageFilesFailedDeleting = 0;
|
|
3583
|
+
const deleteStorageFileInstance = await deleteStorageFile({
|
|
3584
|
+
key: firebase.firestoreDummyKey()
|
|
3585
|
+
});
|
|
3586
|
+
await firebase.iterateFirestoreDocumentSnapshotPairs({
|
|
3587
|
+
documentAccessor: storageFileCollection.documentAccessor(),
|
|
3588
|
+
iterateSnapshotPair: async (snapshotPair) => {
|
|
3589
|
+
const { document: storageFileDocument } = snapshotPair;
|
|
3590
|
+
storageFilesVisited++;
|
|
3591
|
+
const deleteStorageFileResult = await deleteStorageFileInstance(storageFileDocument)
|
|
3592
|
+
.then(() => true)
|
|
3593
|
+
.catch(() => false);
|
|
3594
|
+
if (deleteStorageFileResult) {
|
|
3595
|
+
storageFilesDeleted++;
|
|
3596
|
+
}
|
|
3597
|
+
else {
|
|
3598
|
+
storageFilesFailedDeleting++;
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
constraintsFactory: () => firebase.storageFilesQueuedForDeleteQuery(),
|
|
3602
|
+
queryFactory: storageFileCollection,
|
|
3603
|
+
batchSize: undefined,
|
|
3604
|
+
performTasksConfig: {
|
|
3605
|
+
maxParallelTasks: 10
|
|
3606
|
+
}
|
|
3607
|
+
});
|
|
3608
|
+
const result = {
|
|
3609
|
+
storageFilesDeleted,
|
|
3610
|
+
storageFilesFailedDeleting,
|
|
3611
|
+
storageFilesVisited
|
|
3612
|
+
};
|
|
3613
|
+
return result;
|
|
3614
|
+
};
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
function deleteStorageFileFactory(context) {
|
|
3618
|
+
const { firestoreContext, storageService, storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3619
|
+
const syncStorageFileWithGroupsInTransaction = _syncStorageFileWithGroupsInTransactionFactory(context);
|
|
3620
|
+
return firebaseServerActionTransformFunctionFactory(firebase.DeleteStorageFileParams, async (params) => {
|
|
3621
|
+
const { force } = params;
|
|
3622
|
+
return async (inputStorageFileDocument) => {
|
|
3623
|
+
await firestoreContext.runTransaction(async (transaction) => {
|
|
3624
|
+
const storageFileDocument = await storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(inputStorageFileDocument);
|
|
3625
|
+
const storageFile = await firebaseServer.assertSnapshotData(storageFileDocument);
|
|
3626
|
+
const fileAccessor = storageService.file(storageFile);
|
|
3627
|
+
if (!force) {
|
|
3628
|
+
if (!storageFile.sdat) {
|
|
3629
|
+
throw storageFileNotFlaggedForDeletionError();
|
|
3630
|
+
}
|
|
3631
|
+
else if (!util.isPast(storageFile.sdat)) {
|
|
3632
|
+
throw storageFileCannotBeDeletedYetError();
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
// remove the storage file from any groups
|
|
3636
|
+
await syncStorageFileWithGroupsInTransaction({ storageFileDocument, storageFile, force: true, removeAllStorageFileGroups: true }, transaction);
|
|
3637
|
+
// delete the file
|
|
3638
|
+
await fileAccessor.delete().catch(() => null);
|
|
3639
|
+
// delete the document
|
|
3640
|
+
await storageFileDocument.accessor.delete();
|
|
3641
|
+
});
|
|
3642
|
+
};
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
function downloadStorageFileFactory(context) {
|
|
3646
|
+
const { storageService, firebaseServerActionTransformFunctionFactory, storageFileCollection } = context;
|
|
3647
|
+
return firebaseServerActionTransformFunctionFactory(firebase.DownloadStorageFileParams, async (params) => {
|
|
3648
|
+
const { key: targetStorageFileDocumentKey, asAdmin, expiresAt, expiresIn: inputExpiresIn, responseDisposition, responseContentType } = params;
|
|
3649
|
+
return async (storageFileDocument) => {
|
|
3650
|
+
// if the StorageFileDocument was not provided, set it from the target key
|
|
3651
|
+
if (!storageFileDocument) {
|
|
3652
|
+
storageFileDocument = storageFileCollection.documentAccessor().loadDocumentForKey(targetStorageFileDocumentKey);
|
|
3653
|
+
}
|
|
3654
|
+
const storageFile = await firebaseServer.assertSnapshotData(storageFileDocument);
|
|
3655
|
+
const fileAccessor = storageService.file(storageFile);
|
|
3656
|
+
let result;
|
|
3657
|
+
if (fileAccessor.getSignedUrl) {
|
|
3658
|
+
const expiresIn = inputExpiresIn ?? util.MS_IN_MINUTE * 30;
|
|
3659
|
+
const expires = util.expirationDetails({ defaultExpiresFromDateToNow: true, expiresAt, expiresIn });
|
|
3660
|
+
let downloadUrlExpiresAt = expires.getExpirationDate();
|
|
3661
|
+
// if they're not an admin, limit the expiration to a max of 30 days.
|
|
3662
|
+
if (downloadUrlExpiresAt && !asAdmin) {
|
|
3663
|
+
const maxExpirationDate = dateFns.addDays(new Date(), 30);
|
|
3664
|
+
downloadUrlExpiresAt = date.findMinDate([downloadUrlExpiresAt, maxExpirationDate]);
|
|
3665
|
+
}
|
|
3666
|
+
const [downloadUrl, metadata] = await Promise.all([
|
|
3667
|
+
fileAccessor.getSignedUrl({
|
|
3668
|
+
action: 'read',
|
|
3669
|
+
expiresAt: downloadUrlExpiresAt ?? undefined,
|
|
3670
|
+
responseDisposition: responseDisposition ?? undefined, // can be set by anyone
|
|
3671
|
+
responseType: asAdmin ? (responseContentType ?? undefined) : undefined // can only be set by admins
|
|
3672
|
+
}),
|
|
3673
|
+
fileAccessor.getMetadata()
|
|
3674
|
+
]);
|
|
3675
|
+
result = {
|
|
3676
|
+
url: downloadUrl,
|
|
3677
|
+
fileName: metadata.name ? util.slashPathDetails(metadata.name).end : undefined,
|
|
3678
|
+
mimeType: responseContentType ?? metadata.contentType,
|
|
3679
|
+
expiresAt: util.unixDateTimeSecondsNumberFromDate(downloadUrlExpiresAt)
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
else {
|
|
3683
|
+
throw firebaseServer.internalServerError('Signed url function appears to not be avalable.');
|
|
3684
|
+
}
|
|
3685
|
+
return result;
|
|
3686
|
+
};
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
function createStorageFileGroupInTransactionFactory(context) {
|
|
3690
|
+
const { storageFileGroupCollection } = context;
|
|
3691
|
+
return async (params, transaction) => {
|
|
3692
|
+
const { now: inputNow, skipCreate, template } = params;
|
|
3693
|
+
const now = inputNow ?? new Date();
|
|
3694
|
+
const storageFileGroupDocument = firebase.loadStorageFileGroupDocumentForReferencePair(params, storageFileGroupCollection.documentAccessorForTransaction(transaction));
|
|
3695
|
+
const storageFileGroupTemplate = {
|
|
3696
|
+
o: firebase.firestoreDummyKey(), // set during initialization
|
|
3697
|
+
cat: now,
|
|
3698
|
+
s: true, // requires initialization
|
|
3699
|
+
f: [],
|
|
3700
|
+
...template
|
|
3701
|
+
};
|
|
3702
|
+
if (!skipCreate) {
|
|
3703
|
+
await storageFileGroupDocument.create(storageFileGroupTemplate);
|
|
3704
|
+
}
|
|
3705
|
+
return {
|
|
3706
|
+
storageFileGroupTemplate,
|
|
3707
|
+
storageFileGroupDocument
|
|
3708
|
+
};
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
function createStorageFileGroupFactory(context) {
|
|
3712
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
3713
|
+
const createStorageFileGroupInTransaction = createStorageFileGroupInTransactionFactory(context);
|
|
3714
|
+
return firebaseServerActionTransformFunctionFactory(firebase.CreateStorageFileGroupParams, async (params) => {
|
|
3715
|
+
const { model, storageFileId } = params;
|
|
3716
|
+
const storageFileGroupRelatedModelKey = model ? model : storageFileId ? firebase.inferKeyFromTwoWayFlatFirestoreModelKey(storageFileId) : undefined;
|
|
3717
|
+
if (!storageFileGroupRelatedModelKey) {
|
|
3718
|
+
throw createStorageFileGroupInputError();
|
|
3719
|
+
}
|
|
3720
|
+
return async () => {
|
|
3721
|
+
const result = await firestoreContext.runTransaction(async (transaction) => {
|
|
3722
|
+
const { storageFileGroupDocument } = await createStorageFileGroupInTransaction({ storageFileGroupRelatedModelKey }, transaction);
|
|
3723
|
+
return storageFileGroupDocument;
|
|
3724
|
+
});
|
|
3725
|
+
return result;
|
|
3726
|
+
};
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
function _syncStorageFileWithGroupsInTransactionFactory(context) {
|
|
3730
|
+
const { storageFileCollection, storageFileGroupCollection } = context;
|
|
3731
|
+
const createStorageFileGroupInTransaction = createStorageFileGroupInTransactionFactory(context);
|
|
3732
|
+
return async (input, transaction) => {
|
|
3733
|
+
const { storageFileDocument, storageFile: inputStorageFile, force, removeAllStorageFileGroups, skipStorageFileUpdate } = input;
|
|
3734
|
+
const storageFileDocumentInTransaction = storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileDocument);
|
|
3735
|
+
const storageFileGroupDocumentAccessor = storageFileGroupCollection.documentAccessorForTransaction(transaction);
|
|
3736
|
+
const storageFile = inputStorageFile ?? (await firebaseServer.assertSnapshotData(storageFileDocumentInTransaction));
|
|
3737
|
+
if (!storageFile.gs && !force) {
|
|
3738
|
+
throw storageFileNotFlaggedForGroupsSyncError();
|
|
3739
|
+
}
|
|
3740
|
+
const g = storageFile.g ?? [];
|
|
3741
|
+
const storageFileGroupDocuments = firebase.loadDocumentsForIds(storageFileGroupDocumentAccessor, g);
|
|
3742
|
+
const storageFileGroupPairs = await firebase.getDocumentSnapshotDataPairs(storageFileGroupDocuments);
|
|
3743
|
+
let storageFilesGroupsCreated = 0;
|
|
3744
|
+
let storageFilesGroupsUpdated = 0;
|
|
3745
|
+
await util.performAsyncTasks(storageFileGroupPairs, async (storageFileGroupPair) => {
|
|
3746
|
+
const { data: storageFileGroup, document: storageFileGroupDocument } = storageFileGroupPair;
|
|
3747
|
+
const existsInStorageFileGroup = storageFileGroup?.f.some((x) => x.s === storageFileDocument.id);
|
|
3748
|
+
const change = removeAllStorageFileGroups ? (existsInStorageFileGroup ? 'remove' : undefined) : !existsInStorageFileGroup ? 'add' : undefined;
|
|
3749
|
+
switch (change) {
|
|
3750
|
+
case 'add':
|
|
3751
|
+
// add it if it doesn't exist
|
|
3752
|
+
const createTemplate = firebase.calculateStorageFileGroupEmbeddedFileUpdate({
|
|
3753
|
+
storageFileGroup: storageFileGroup ?? { f: [] },
|
|
3754
|
+
insert: [
|
|
3755
|
+
{
|
|
3756
|
+
s: storageFileDocument.id
|
|
3757
|
+
}
|
|
3758
|
+
],
|
|
3759
|
+
allowRecalculateRegenerateFlag: false
|
|
3760
|
+
});
|
|
3761
|
+
if (!storageFileGroup) {
|
|
3762
|
+
// if the group does not exist, then create it
|
|
3763
|
+
await createStorageFileGroupInTransaction({ storageFileGroupDocument, template: createTemplate }, transaction);
|
|
3764
|
+
storageFilesGroupsCreated += 1;
|
|
3765
|
+
}
|
|
3766
|
+
else {
|
|
3767
|
+
// if the group exists, then update it
|
|
3768
|
+
await storageFileGroupDocument.update(createTemplate);
|
|
3769
|
+
storageFilesGroupsUpdated += 1;
|
|
3770
|
+
}
|
|
3771
|
+
break;
|
|
3772
|
+
case 'remove':
|
|
3773
|
+
// remove it
|
|
3774
|
+
const removeTemplate = firebase.calculateStorageFileGroupEmbeddedFileUpdate({
|
|
3775
|
+
storageFileGroup: storageFileGroup ?? { f: [] },
|
|
3776
|
+
remove: [storageFileDocument.id]
|
|
3777
|
+
});
|
|
3778
|
+
await storageFileGroupDocument.update(removeTemplate);
|
|
3779
|
+
storageFilesGroupsUpdated += 1;
|
|
3780
|
+
break;
|
|
3781
|
+
}
|
|
3782
|
+
});
|
|
3783
|
+
const result = {
|
|
3784
|
+
storageFilesGroupsCreated,
|
|
3785
|
+
storageFilesGroupsUpdated
|
|
3786
|
+
};
|
|
3787
|
+
// update the storage file to no longer be flagged for sync
|
|
3788
|
+
if (!skipStorageFileUpdate) {
|
|
3789
|
+
await storageFileDocumentInTransaction.update({
|
|
3790
|
+
gs: false
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
return result;
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
function syncStorageFileWithGroupsFactory(context) {
|
|
3797
|
+
const { firestoreContext, storageFileCollection, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3798
|
+
const syncStorageFileWithGroupsInTransaction = _syncStorageFileWithGroupsInTransactionFactory(context);
|
|
3799
|
+
return firebaseServerActionTransformFunctionFactory(firebase.SyncStorageFileWithGroupsParams, async (params) => {
|
|
3800
|
+
const { force } = params;
|
|
3801
|
+
return async (storageFileDocument) => {
|
|
3802
|
+
return firestoreContext.runTransaction(async (transaction) => syncStorageFileWithGroupsInTransaction({ storageFileDocument, force }, transaction));
|
|
3803
|
+
};
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
function syncAllFlaggedStorageFilesWithGroupsFactory(context) {
|
|
3807
|
+
const { firebaseServerActionTransformFunctionFactory, storageFileCollection } = context;
|
|
3808
|
+
const syncStorageFileWithGroups = syncStorageFileWithGroupsFactory(context);
|
|
3809
|
+
return firebaseServerActionTransformFunctionFactory(firebase.SyncAllFlaggedStorageFilesWithGroupsParams, async (params) => {
|
|
3810
|
+
return async () => {
|
|
3811
|
+
const syncStorageFileWithGroupsInstance = await syncStorageFileWithGroups({
|
|
3812
|
+
key: firebase.firestoreDummyKey(),
|
|
3813
|
+
force: true // force anyways; they should all be flagged for sync when the query hits
|
|
3814
|
+
});
|
|
3815
|
+
let storageFilesSynced = 0;
|
|
3816
|
+
let storageFilesGroupsCreated = 0;
|
|
3817
|
+
let storageFilesGroupsUpdated = 0;
|
|
3818
|
+
await firebase.iterateFirestoreDocumentSnapshotPairBatches({
|
|
3819
|
+
documentAccessor: storageFileCollection.documentAccessor(),
|
|
3820
|
+
iterateSnapshotPairsBatch: async (snapshotPairBatch) => {
|
|
3821
|
+
// only sync StorageFiles that are flagged for sync
|
|
3822
|
+
await util.runAsyncTasksForValues(snapshotPairBatch.filter((x) => x.data.gs), async (snapshotPair) => {
|
|
3823
|
+
const { document: storageFileDocument } = snapshotPair;
|
|
3824
|
+
const result = await syncStorageFileWithGroupsInstance(storageFileDocument);
|
|
3825
|
+
storageFilesSynced += 1;
|
|
3826
|
+
storageFilesGroupsCreated += result.storageFilesGroupsCreated;
|
|
3827
|
+
storageFilesGroupsUpdated += result.storageFilesGroupsUpdated;
|
|
3828
|
+
}, {
|
|
3829
|
+
maxParallelTasks: 10, // can update 10 storageFiles/Groups at the same time
|
|
3830
|
+
nonConcurrentTaskKeyFactory: (x) => x.data.g // do not update the same group at the same time
|
|
3831
|
+
});
|
|
3832
|
+
},
|
|
3833
|
+
queryFactory: storageFileCollection,
|
|
3834
|
+
constraintsFactory: () => firebase.storageFileFlaggedForSyncWithGroupsQuery(),
|
|
3835
|
+
performTasksConfig: {
|
|
3836
|
+
sequential: true // run batches sequentially to avoid contention in updating a StorageFileGroup
|
|
3837
|
+
},
|
|
3838
|
+
totalSnapshotsLimit: 1000,
|
|
3839
|
+
limitPerCheckpoint: 100
|
|
3840
|
+
});
|
|
3841
|
+
const result = {
|
|
3842
|
+
storageFilesSynced,
|
|
3843
|
+
storageFilesGroupsCreated,
|
|
3844
|
+
storageFilesGroupsUpdated
|
|
3845
|
+
};
|
|
3846
|
+
return result;
|
|
3847
|
+
};
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
function regenerateStorageFileGroupContentFactory(context) {
|
|
3851
|
+
const { firestoreContext, storageService, storageFileCollection, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
|
|
3852
|
+
const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
|
|
3853
|
+
return firebaseServerActionTransformFunctionFactory(firebase.RegenerateStorageFileGroupContentParams, async (params) => {
|
|
3854
|
+
const { force } = params;
|
|
3855
|
+
const createStorageFileDocumentPair = firebase.createStorageFileDocumentPairFactory({
|
|
3856
|
+
defaultCreationType: firebase.StorageFileCreationType.FOR_STORAGE_FILE_GROUP
|
|
3857
|
+
});
|
|
3858
|
+
return async (storageFileGroupDocument) => {
|
|
3859
|
+
return firestoreContext.runTransaction(async (transaction) => {
|
|
3860
|
+
const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
|
|
3861
|
+
const storageFileGroup = await firebaseServer.assertSnapshotData(storageFileGroupDocumentInTransaction);
|
|
3862
|
+
const storageFileDocumentAccessor = storageFileCollection.documentAccessorForTransaction(transaction);
|
|
3863
|
+
const { o, zsf, s } = storageFileGroup;
|
|
3864
|
+
// must not be queued for initialization
|
|
3865
|
+
if (s) {
|
|
3866
|
+
throw storageFileGroupQueuedForInitializationError();
|
|
3867
|
+
}
|
|
3868
|
+
const existingZipStorageFileDocument = zsf ? storageFileDocumentAccessor.loadDocumentForId(zsf) : undefined;
|
|
3869
|
+
const [existingZipStorageFilePair] = await Promise.all([existingZipStorageFileDocument ? firebase.getDocumentSnapshotDataPair(existingZipStorageFileDocument) : undefined]);
|
|
3870
|
+
let contentStorageFilesFlaggedForProcessing = 0;
|
|
3871
|
+
const updateTemplate = {
|
|
3872
|
+
re: false // clear the regeneration flag
|
|
3873
|
+
};
|
|
3874
|
+
// For each content type, create/update/flag the StorageFile for processing that type
|
|
3875
|
+
const { regenerateZip } = firebase.calculateStorageFileGroupRegeneration({ storageFileGroup, force });
|
|
3876
|
+
if (regenerateZip) {
|
|
3877
|
+
// check that the storageFile exists, and if it doesn't, create a new one
|
|
3878
|
+
if (!existingZipStorageFilePair?.data) {
|
|
3879
|
+
const zipStorageFile = storageService.file(firebase.storageFileGroupZipFileStoragePath(storageFileGroupDocument.id));
|
|
3880
|
+
// create a new StorageFile
|
|
3881
|
+
const { storageFileDocument } = await createStorageFileDocumentPair({
|
|
3882
|
+
storagePathRef: zipStorageFile,
|
|
3883
|
+
accessor: storageFileDocumentAccessor,
|
|
3884
|
+
parentStorageFileGroup: storageFileGroupDocument,
|
|
3885
|
+
purpose: firebase.STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE,
|
|
3886
|
+
shouldBeProcessed: true,
|
|
3887
|
+
ownershipKey: o,
|
|
3888
|
+
metadata: {
|
|
3889
|
+
sfg: storageFileGroupDocument.id
|
|
3890
|
+
}
|
|
3891
|
+
});
|
|
3892
|
+
updateTemplate.zsf = storageFileDocument.id;
|
|
3893
|
+
}
|
|
3894
|
+
else {
|
|
3895
|
+
// flag it for processing again
|
|
3896
|
+
await processStorageFileInTransaction({ params: { processAgainIfSuccessful: true }, storageFileDocument: existingZipStorageFilePair.document, storageFile: existingZipStorageFilePair.data }, transaction);
|
|
3897
|
+
}
|
|
3898
|
+
contentStorageFilesFlaggedForProcessing += 1;
|
|
3899
|
+
}
|
|
3900
|
+
// update the StorageFileGroup
|
|
3901
|
+
await storageFileGroupDocumentInTransaction.update(updateTemplate);
|
|
3902
|
+
const result = {
|
|
3903
|
+
contentStorageFilesFlaggedForProcessing
|
|
3904
|
+
};
|
|
3905
|
+
return result;
|
|
3906
|
+
});
|
|
3907
|
+
};
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
function regenerateAllFlaggedStorageFileGroupsContentFactory(context) {
|
|
3911
|
+
const { firebaseServerActionTransformFunctionFactory, storageFileGroupCollection } = context;
|
|
3912
|
+
const regenerateStorageFileGroupContent = regenerateStorageFileGroupContentFactory(context);
|
|
3913
|
+
return firebaseServerActionTransformFunctionFactory(firebase.RegenerateAllFlaggedStorageFileGroupsContentParams, async (params) => {
|
|
3914
|
+
return async () => {
|
|
3915
|
+
const regenerateStorageFileGroupContentInstance = await regenerateStorageFileGroupContent({
|
|
3916
|
+
key: firebase.firestoreDummyKey()
|
|
3917
|
+
});
|
|
3918
|
+
let storageFileGroupsUpdated = 0;
|
|
3919
|
+
let contentStorageFilesFlaggedForProcessing = 0;
|
|
3920
|
+
await firebase.iterateFirestoreDocumentSnapshotPairs({
|
|
3921
|
+
documentAccessor: storageFileGroupCollection.documentAccessor(),
|
|
3922
|
+
iterateSnapshotPair: async (snapshotPair) => {
|
|
3923
|
+
const { data: storageFileGroup } = snapshotPair;
|
|
3924
|
+
if (!storageFileGroup.s) {
|
|
3925
|
+
const result = await regenerateStorageFileGroupContentInstance(snapshotPair.document);
|
|
3926
|
+
storageFileGroupsUpdated += 1;
|
|
3927
|
+
contentStorageFilesFlaggedForProcessing += result.contentStorageFilesFlaggedForProcessing;
|
|
3928
|
+
}
|
|
3929
|
+
},
|
|
3930
|
+
queryFactory: storageFileGroupCollection,
|
|
3931
|
+
constraintsFactory: () => firebase.storageFileGroupsFlaggedForContentRegenerationQuery(),
|
|
3932
|
+
performTasksConfig: {
|
|
3933
|
+
maxParallelTasks: 10
|
|
3934
|
+
},
|
|
3935
|
+
totalSnapshotsLimit: 1000,
|
|
3936
|
+
limitPerCheckpoint: 100
|
|
3937
|
+
});
|
|
3938
|
+
const result = {
|
|
3939
|
+
storageFileGroupsUpdated,
|
|
3940
|
+
contentStorageFilesFlaggedForProcessing
|
|
3941
|
+
};
|
|
3942
|
+
return result;
|
|
3943
|
+
};
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// MARK: StorageFileInitServerActionsContextConfig
|
|
3948
|
+
/**
|
|
3949
|
+
* Token to access/override the StorageFileTemplateService's defaults records.
|
|
3950
|
+
*/
|
|
3951
|
+
const STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = 'STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG';
|
|
3952
|
+
const MAKE_TEMPLATE_FOR_STORAGEFILE_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = false;
|
|
3953
|
+
class StorageFileInitServerActions {
|
|
3954
|
+
}
|
|
3955
|
+
function storageFileInitServerActions(context) {
|
|
3956
|
+
return {
|
|
3957
|
+
initializeStorageFileGroup: initializeStorageFileGroupFactory(context),
|
|
3958
|
+
initializeAllApplicableStorageFileGroups: initializeAllApplicableStorageFileGroupsFactory(context)
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
async function initializeStorageFileModelInTransaction(input) {
|
|
3962
|
+
const { makeTemplateFunction, throwErrorIfAlreadyInitialized, transaction, document: documentInTransaction, data: storageFileModel } = input;
|
|
3963
|
+
let initialized = false;
|
|
3964
|
+
const alreadyInitialized = !storageFileModel.s;
|
|
3965
|
+
if (!alreadyInitialized) {
|
|
3966
|
+
const flatModelKey = documentInTransaction.id;
|
|
3967
|
+
const modelKey = firebase.inferKeyFromTwoWayFlatFirestoreModelKey(flatModelKey);
|
|
3968
|
+
const modelCollectionName = firebase.firestoreModelKeyCollectionName(modelKey);
|
|
3969
|
+
const input = {
|
|
3970
|
+
transaction,
|
|
3971
|
+
flatModelKey,
|
|
3972
|
+
modelKey,
|
|
3973
|
+
collectionName: modelCollectionName
|
|
3974
|
+
};
|
|
3975
|
+
const template = await makeTemplateFunction(input);
|
|
3976
|
+
if (template === false) {
|
|
3977
|
+
await documentInTransaction.accessor.delete();
|
|
3978
|
+
}
|
|
3979
|
+
else if (template == null) {
|
|
3980
|
+
await documentInTransaction.update({
|
|
3981
|
+
s: false, // set false when "f" is set true
|
|
3982
|
+
fi: true
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
else {
|
|
3986
|
+
initialized = true;
|
|
3987
|
+
await documentInTransaction.update({
|
|
3988
|
+
//
|
|
3989
|
+
...template,
|
|
3990
|
+
m: undefined, // should not be changed
|
|
3991
|
+
s: null, // is now initialized.
|
|
3992
|
+
fi: false // set false
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
else if (throwErrorIfAlreadyInitialized) {
|
|
3997
|
+
throw storageFileModelAlreadyInitializedError();
|
|
3998
|
+
}
|
|
3999
|
+
return {
|
|
4000
|
+
initialized,
|
|
4001
|
+
alreadyInitialized
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function initializeStorageFileGroupInTransactionFactory(context) {
|
|
4005
|
+
const { storageFileGroupCollection, makeTemplateForStorageFileGroupInitialization } = context;
|
|
4006
|
+
return async (params, storageFileGroupDocument, transaction) => {
|
|
4007
|
+
const { throwErrorIfAlreadyInitialized } = params;
|
|
4008
|
+
const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
|
|
4009
|
+
const storageFileGroup = await firebaseServer.assertSnapshotData(storageFileGroupDocumentInTransaction);
|
|
4010
|
+
return initializeStorageFileModelInTransaction({
|
|
4011
|
+
makeTemplateFunction: async (input) => {
|
|
4012
|
+
const baseTemplate = (await makeTemplateForStorageFileGroupInitialization(input));
|
|
4013
|
+
// template can only define o and any StorageFileGroupContentFlagsData properties
|
|
4014
|
+
return {
|
|
4015
|
+
o: baseTemplate.o,
|
|
4016
|
+
z: baseTemplate.z,
|
|
4017
|
+
re: true // always flag for content regeneration
|
|
4018
|
+
};
|
|
4019
|
+
},
|
|
4020
|
+
throwErrorIfAlreadyInitialized,
|
|
4021
|
+
transaction,
|
|
4022
|
+
document: storageFileGroupDocumentInTransaction,
|
|
4023
|
+
data: storageFileGroup
|
|
4024
|
+
});
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
function initializeStorageFileGroupFactory(context) {
|
|
4028
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
|
|
4029
|
+
const initializeStorageFileGroupInTransaction = initializeStorageFileGroupInTransactionFactory(context);
|
|
4030
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeStorageFileModelParams, async (params) => {
|
|
4031
|
+
return async (storageFileGroupDocument) => {
|
|
4032
|
+
await firestoreContext.runTransaction((transaction) => initializeStorageFileGroupInTransaction(params, storageFileGroupDocument, transaction));
|
|
4033
|
+
return storageFileGroupDocument;
|
|
4034
|
+
};
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
function initializeAllApplicableStorageFileGroupsFactory(context) {
|
|
4038
|
+
const { firestoreContext, firebaseServerActionTransformFunctionFactory, storageFileGroupCollection } = context;
|
|
4039
|
+
const initializeStorageFileGroupInTransaction = initializeStorageFileGroupInTransactionFactory(context);
|
|
4040
|
+
return firebaseServerActionTransformFunctionFactory(firebase.InitializeAllApplicableStorageFileGroupsParams, async () => {
|
|
4041
|
+
return async () => {
|
|
4042
|
+
let storageFileGroupsVisited = 0;
|
|
4043
|
+
let storageFileGroupsSucceeded = 0;
|
|
4044
|
+
let storageFileGroupsFailed = 0;
|
|
4045
|
+
let storageFileGroupsAlreadyInitialized = 0;
|
|
4046
|
+
const initializeStorageFileGroupParams = { key: firebase.firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
|
|
4047
|
+
async function initializeStorageFileGroups() {
|
|
4048
|
+
const query = storageFileGroupCollection.queryDocument(firebase.storageFileGroupsFlaggedForNeedsInitializationQuery());
|
|
4049
|
+
const storageFileGroupDocuments = await query.getDocs();
|
|
4050
|
+
const result = await util.performAsyncTasks(storageFileGroupDocuments, async (storageFileGroupDocument) => {
|
|
4051
|
+
return firestoreContext.runTransaction((transaction) => initializeStorageFileGroupInTransaction(initializeStorageFileGroupParams, storageFileGroupDocument, transaction));
|
|
4052
|
+
}, {
|
|
4053
|
+
maxParallelTasks: 5
|
|
4054
|
+
});
|
|
4055
|
+
return result;
|
|
4056
|
+
}
|
|
4057
|
+
while (true) {
|
|
4058
|
+
const initializeStorageFileGroupsResults = await initializeStorageFileGroups();
|
|
4059
|
+
initializeStorageFileGroupsResults.results.forEach((x) => {
|
|
4060
|
+
const result = x[1];
|
|
4061
|
+
if (result.alreadyInitialized) {
|
|
4062
|
+
storageFileGroupsAlreadyInitialized += 1;
|
|
4063
|
+
}
|
|
4064
|
+
else if (result.initialized) {
|
|
4065
|
+
storageFileGroupsSucceeded += 1;
|
|
4066
|
+
}
|
|
4067
|
+
else {
|
|
4068
|
+
storageFileGroupsFailed += 1;
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
const found = initializeStorageFileGroupsResults.results.length;
|
|
4072
|
+
storageFileGroupsVisited += found;
|
|
4073
|
+
if (!found) {
|
|
4074
|
+
break;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
const result = {
|
|
4078
|
+
storageFileGroupsVisited,
|
|
4079
|
+
storageFileGroupsSucceeded,
|
|
4080
|
+
storageFileGroupsFailed,
|
|
4081
|
+
storageFileGroupsAlreadyInitialized
|
|
4082
|
+
};
|
|
4083
|
+
return result;
|
|
4084
|
+
};
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
/**
|
|
4089
|
+
* Service dedicated to initializing a StorageFileDocument value from an uploaded file.
|
|
4090
|
+
*/
|
|
4091
|
+
class StorageFileInitializeFromUploadService {
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
// MARK: Provider Factories
|
|
4095
|
+
function storageFileServerActionsContextFactory(context, storageFileInitializeFromUploadService) {
|
|
4096
|
+
return { ...context, storageFileInitializeFromUploadService };
|
|
4097
|
+
}
|
|
4098
|
+
function storageFileServerActionsFactory(context) {
|
|
4099
|
+
return storageFileServerActions(context);
|
|
4100
|
+
}
|
|
4101
|
+
function storageFileInitServerActionsFactory(context, storageFileInitServerActionsContextConfig) {
|
|
4102
|
+
return storageFileInitServerActions({
|
|
4103
|
+
...context,
|
|
4104
|
+
...storageFileInitServerActionsContextConfig
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* Convenience function used to generate ModuleMetadata for an app's StorageFileModule.
|
|
4109
|
+
*
|
|
4110
|
+
* By default this module exports:
|
|
4111
|
+
* - StorageFileServerActionContext (STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN)
|
|
4112
|
+
* - StorageFileServerActions
|
|
4113
|
+
* - StorageFileInitServerActions
|
|
4114
|
+
*
|
|
4115
|
+
* Be sure the class that delares the module using this function also extends AbstractAppStorageFileModule.
|
|
4116
|
+
*
|
|
4117
|
+
* @param provide
|
|
4118
|
+
* @param useFactory
|
|
4119
|
+
* @returns
|
|
4120
|
+
*/
|
|
4121
|
+
function appStorageFileModuleMetadata(config$1) {
|
|
4122
|
+
const { dependencyModule, imports, exports: exports$1, providers } = config$1;
|
|
4123
|
+
const dependencyModuleImport = dependencyModule ? [dependencyModule] : [];
|
|
4124
|
+
return {
|
|
4125
|
+
imports: [config.ConfigModule, ...dependencyModuleImport, ...(imports ?? [])],
|
|
4126
|
+
exports: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, StorageFileServerActions, StorageFileInitServerActions, ...(exports$1 ?? [])],
|
|
4127
|
+
providers: [
|
|
4128
|
+
{
|
|
4129
|
+
provide: STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN,
|
|
4130
|
+
useFactory: storageFileServerActionsContextFactory,
|
|
4131
|
+
inject: [BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, StorageFileInitializeFromUploadService]
|
|
4132
|
+
},
|
|
4133
|
+
{
|
|
4134
|
+
provide: StorageFileServerActions,
|
|
4135
|
+
useFactory: storageFileServerActionsFactory,
|
|
4136
|
+
inject: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN]
|
|
4137
|
+
},
|
|
4138
|
+
{
|
|
4139
|
+
provide: StorageFileInitServerActions,
|
|
4140
|
+
useFactory: storageFileInitServerActionsFactory,
|
|
4141
|
+
inject: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN]
|
|
4142
|
+
},
|
|
4143
|
+
...(providers ?? [])
|
|
4144
|
+
]
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
/**
|
|
4149
|
+
* Performs a query and flags the matching StorageFiles for deletion.
|
|
4150
|
+
*
|
|
4151
|
+
* @param input The input for the query and flagging.
|
|
4152
|
+
* @returns The result of the query and flagging.
|
|
4153
|
+
*/
|
|
4154
|
+
async function queryAndFlagStorageFilesForDelete(input) {
|
|
4155
|
+
const { storageFileCollection, constraints, queuedForDeleteTime: inputQueueForDeleteTime, skipDeleteForKeys } = input;
|
|
4156
|
+
const queuedForDeleteTime = inputQueueForDeleteTime ?? true;
|
|
4157
|
+
const skipDeleteSet = new Set(util.asArray(skipDeleteForKeys));
|
|
4158
|
+
let visitedCount = 0;
|
|
4159
|
+
let queuedForDeleteCount = 0;
|
|
4160
|
+
await firebase.iterateFirestoreDocumentSnapshotPairs({
|
|
4161
|
+
documentAccessor: storageFileCollection.documentAccessor(),
|
|
4162
|
+
iterateSnapshotPair: async (snapshotPair) => {
|
|
4163
|
+
const { document, data: storageFile } = snapshotPair;
|
|
4164
|
+
if (!storageFile.sdat && !skipDeleteSet.has(storageFile.key)) {
|
|
4165
|
+
await document.update(markStorageFileForDeleteTemplate(queuedForDeleteTime));
|
|
4166
|
+
queuedForDeleteCount++;
|
|
4167
|
+
}
|
|
4168
|
+
visitedCount++;
|
|
4169
|
+
},
|
|
4170
|
+
queryFactory: storageFileCollection,
|
|
4171
|
+
constraintsFactory: () => constraints,
|
|
4172
|
+
batchSize: undefined,
|
|
4173
|
+
performTasksConfig: {
|
|
4174
|
+
maxParallelTasks: 20
|
|
4175
|
+
}
|
|
4176
|
+
});
|
|
4177
|
+
return {
|
|
4178
|
+
visitedCount,
|
|
4179
|
+
queuedForDeleteCount
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Creates a template for updating a StorageFile to be queued for deletion at the input time.
|
|
4184
|
+
*
|
|
4185
|
+
* @param queueForDeleteTime When to delete the StorageFile. If true or unset, the StorageFile will be flagged to be deleted immediately.
|
|
4186
|
+
* @returns The update template for the StorageFile.
|
|
4187
|
+
*/
|
|
4188
|
+
function markStorageFileForDeleteTemplate(queueForDeleteTime) {
|
|
4189
|
+
const updateTemplate = {
|
|
4190
|
+
sdat: queueForDeleteTime === true || queueForDeleteTime == null ? new Date() : util.dateFromDateOrTimeMillisecondsNumber(queueForDeleteTime),
|
|
4191
|
+
fs: firebase.StorageFileState.QUEUED_FOR_DELETE
|
|
4192
|
+
};
|
|
4193
|
+
return updateTemplate;
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
const storageFileProcessingNotificationTaskHandlerDefaultCleanup = () => {
|
|
4197
|
+
return {
|
|
4198
|
+
cleanupSuccess: true,
|
|
4199
|
+
nextProcessingState: firebase.StorageFileProcessingState.SUCCESS,
|
|
4200
|
+
queueForDelete: false // do not queue for delete automatically
|
|
4201
|
+
};
|
|
4202
|
+
};
|
|
4203
|
+
/**
|
|
4204
|
+
* Creates a NotificationTaskServiceTaskHandlerConfig that handles the StorageFileProcessingNotificationTask.
|
|
4205
|
+
*/
|
|
4206
|
+
function storageFileProcessingNotificationTaskHandler(config) {
|
|
4207
|
+
const { processors: inputProcessors, storageAccessor, storageFileFirestoreCollections, allStorageFileGroupProcessorConfig } = config;
|
|
4208
|
+
const storageFileDocumentAccessor = storageFileFirestoreCollections.storageFileCollection.documentAccessor();
|
|
4209
|
+
const makeFileDetailsAccessor = firebase.storedFileReaderFactory();
|
|
4210
|
+
const defaultCleanup = storageFileProcessingNotificationTaskHandlerDefaultCleanup;
|
|
4211
|
+
const processors = [...inputProcessors];
|
|
4212
|
+
if (allStorageFileGroupProcessorConfig !== false) {
|
|
4213
|
+
const storageFileGroupProcessors = allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors({
|
|
4214
|
+
...allStorageFileGroupProcessorConfig,
|
|
4215
|
+
storageFileFirestoreCollections,
|
|
4216
|
+
storageAccessor
|
|
4217
|
+
});
|
|
4218
|
+
util.pushArrayItemsIntoArray(processors, storageFileGroupProcessors);
|
|
4219
|
+
}
|
|
4220
|
+
return notificationTaskSubtaskNotificationTaskHandlerFactory({
|
|
4221
|
+
taskType: firebase.STORAGE_FILE_PROCESSING_NOTIFICATION_TASK_TYPE,
|
|
4222
|
+
subtaskHandlerFunctionName: 'storageFileProcessingNotificationTaskHandler',
|
|
4223
|
+
inputFunction: async (data) => {
|
|
4224
|
+
const storageFileDocument = await storageFileDocumentAccessor.loadDocumentForId(data.storageFile);
|
|
4225
|
+
const loadStorageFile = util.cachedGetter(async () => {
|
|
4226
|
+
const storageFile = await firebase.getDocumentSnapshotData(storageFileDocument, true);
|
|
4227
|
+
if (!storageFile) {
|
|
4228
|
+
throw notificationTaskSubTaskMissingRequiredDataTermination();
|
|
4229
|
+
}
|
|
4230
|
+
return storageFile;
|
|
4231
|
+
});
|
|
4232
|
+
let purpose = data?.p;
|
|
4233
|
+
if (!purpose) {
|
|
4234
|
+
// attempt to load the purpose from the storage file, if it exists.
|
|
4235
|
+
purpose = (await loadStorageFile().then((x) => x.p));
|
|
4236
|
+
}
|
|
4237
|
+
let storagePath;
|
|
4238
|
+
if (data.storagePath) {
|
|
4239
|
+
storagePath = data.storagePath;
|
|
4240
|
+
}
|
|
4241
|
+
else {
|
|
4242
|
+
storagePath = await loadStorageFile().then((x) => ({ bucketId: x.bucketId, pathString: x.pathString }));
|
|
4243
|
+
}
|
|
4244
|
+
const file = storageAccessor.file(storagePath);
|
|
4245
|
+
const fileDetailsAccessor = makeFileDetailsAccessor(file);
|
|
4246
|
+
const input = {
|
|
4247
|
+
target: purpose,
|
|
4248
|
+
loadStorageFile,
|
|
4249
|
+
fileDetailsAccessor,
|
|
4250
|
+
storageFileDocument
|
|
4251
|
+
};
|
|
4252
|
+
return input;
|
|
4253
|
+
},
|
|
4254
|
+
buildUpdateMetadata: (baseUpdateMetadata, input) => {
|
|
4255
|
+
const { target } = input;
|
|
4256
|
+
return {
|
|
4257
|
+
...baseUpdateMetadata,
|
|
4258
|
+
// always re-copy the target/storagePath for the next run so StorageFile does not have to be reloaded
|
|
4259
|
+
p: target,
|
|
4260
|
+
storagePath: firebase.copyStoragePath(input.fileDetailsAccessor.input)
|
|
4261
|
+
};
|
|
4262
|
+
},
|
|
4263
|
+
defaultCleanup,
|
|
4264
|
+
cleanupFunction: async function (input, cleanupInstructions) {
|
|
4265
|
+
const { storageFileDocument } = input;
|
|
4266
|
+
const { nextProcessingState, queueForDelete, flagResyncWithStorageFileGroups: syncWithStorageFileGroups } = cleanupInstructions;
|
|
4267
|
+
let updateTemplate = {
|
|
4268
|
+
ps: nextProcessingState ?? firebase.StorageFileProcessingState.SUCCESS,
|
|
4269
|
+
pcat: new Date(), // set new cleanup/completion date
|
|
4270
|
+
pn: null // clear reference
|
|
4271
|
+
};
|
|
4272
|
+
const shouldQueueForDelete = queueForDelete != null && queueForDelete !== false;
|
|
4273
|
+
if (shouldQueueForDelete) {
|
|
4274
|
+
updateTemplate = {
|
|
4275
|
+
...updateTemplate,
|
|
4276
|
+
...markStorageFileForDeleteTemplate(queueForDelete)
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
else if (syncWithStorageFileGroups) {
|
|
4280
|
+
// resync with storage file groups
|
|
4281
|
+
updateTemplate.gs = true;
|
|
4282
|
+
}
|
|
4283
|
+
await storageFileDocument.update(updateTemplate);
|
|
4284
|
+
return firebase.notificationTaskComplete();
|
|
4285
|
+
}
|
|
4286
|
+
})({
|
|
4287
|
+
...config,
|
|
4288
|
+
processors
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
function allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors(config) {
|
|
4292
|
+
const { excludeZipProcessing } = config;
|
|
4293
|
+
const processors = [];
|
|
4294
|
+
if (!excludeZipProcessing) {
|
|
4295
|
+
processors.push(storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(config));
|
|
4296
|
+
}
|
|
4297
|
+
return processors;
|
|
4298
|
+
}
|
|
4299
|
+
function storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(config) {
|
|
4300
|
+
const { storageFileFirestoreCollections, storageAccessor, zip } = config;
|
|
4301
|
+
const { storageFileCollection, storageFileGroupCollection } = storageFileFirestoreCollections;
|
|
4302
|
+
const { maxNumberOfFilesToZipInParallel: inputMaxNumberOfFilesToZipInParallel, zipFileDisplayNameFunctionFactory: inputZipFileDisplayNameFunctionFactory, configureZipInfoJson: inputConfigureZipInfoJson, configureZipArchiverOptions: inputConfigureZipArchiverOptions, finalizeZipArchive } = zip ?? {};
|
|
4303
|
+
const maxNumberOfFilesToZipInParallel = inputMaxNumberOfFilesToZipInParallel ?? 3;
|
|
4304
|
+
const appendZipInfoJson = inputConfigureZipInfoJson !== false;
|
|
4305
|
+
const configureZipArchiverOptions = inputConfigureZipArchiverOptions ?? (() => ({ zlib: { level: 9 } }));
|
|
4306
|
+
const configureZipInfoJson = (appendZipInfoJson ? inputConfigureZipInfoJson : undefined) ?? util.MAP_IDENTITY;
|
|
4307
|
+
const zipFileDisplayNameFunctionFactory = inputZipFileDisplayNameFunctionFactory ?? (() => () => null);
|
|
4308
|
+
const storageFileGroupZipProcessorConfig = {
|
|
4309
|
+
target: firebase.STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE,
|
|
4310
|
+
cleanup: zip?.cleanup,
|
|
4311
|
+
flow: [
|
|
4312
|
+
{
|
|
4313
|
+
subtask: firebase.STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE_CREATE_ZIP_SUBTASK,
|
|
4314
|
+
fn: async (input) => {
|
|
4315
|
+
const { storageFileDocument, fileDetailsAccessor } = input;
|
|
4316
|
+
const storageFile = await input.loadStorageFile();
|
|
4317
|
+
const storageFileMetadata = storageFile.d;
|
|
4318
|
+
const storageFileGroupId = storageFileMetadata?.sfg;
|
|
4319
|
+
let result;
|
|
4320
|
+
async function flagStorageFileForDeletion() {
|
|
4321
|
+
await storageFileDocument.update(markStorageFileForDeleteTemplate());
|
|
4322
|
+
return firebase.notificationTaskComplete(); // skip cleanup step
|
|
4323
|
+
}
|
|
4324
|
+
if (storageFileGroupId) {
|
|
4325
|
+
const storageFileGroupDocument = storageFileGroupCollection.documentAccessor().loadDocumentForId(storageFileGroupId);
|
|
4326
|
+
const storageFileGroup = await storageFileGroupDocument.snapshotData();
|
|
4327
|
+
if (storageFileGroup) {
|
|
4328
|
+
const embeddedFilesMap = new Map(storageFileGroup.f.map((x) => [x.s, x]));
|
|
4329
|
+
const storageFileIdsToZip = storageFileGroup.f.map((x) => x.s);
|
|
4330
|
+
const storageFilesToZip = firebase.loadDocumentsForIds(storageFileCollection.documentAccessor(), storageFileIdsToZip);
|
|
4331
|
+
const storageFileDataPairsToZip = await firebase.getDocumentSnapshotDataPairs(storageFilesToZip);
|
|
4332
|
+
const zipFileDisplayNameFunction = await zipFileDisplayNameFunctionFactory({ storageFileGroup, storageFileGroupDocument });
|
|
4333
|
+
let flagCleanFileAssociations = undefined;
|
|
4334
|
+
// create a new file
|
|
4335
|
+
const zipFileAccessor = storageAccessor.file(fileDetailsAccessor.input);
|
|
4336
|
+
if (zipFileAccessor.uploadStream && zipFileAccessor.getStream) {
|
|
4337
|
+
const uploadStream = zipFileAccessor.uploadStream({
|
|
4338
|
+
contentType: util.ZIP_FILE_MIME_TYPE
|
|
4339
|
+
});
|
|
4340
|
+
const startedAt = new Date();
|
|
4341
|
+
const archiverOptions = await configureZipArchiverOptions({ input, storageFileGroup });
|
|
4342
|
+
const newArchive = archiver('zip', archiverOptions);
|
|
4343
|
+
// pipe the archive to the upload stream
|
|
4344
|
+
newArchive.pipe(uploadStream, { end: true });
|
|
4345
|
+
// upload each of the files to the archive
|
|
4346
|
+
await util.performAsyncTasks(storageFileDataPairsToZip, async (storageFileDataPair) => {
|
|
4347
|
+
const { data: storageFile } = storageFileDataPair;
|
|
4348
|
+
if (storageFile) {
|
|
4349
|
+
const { n: storageFileDisplayName } = storageFile;
|
|
4350
|
+
// make sure it references the storage file group
|
|
4351
|
+
const referencesStorageFileGroup = storageFile.g.some((x) => x === storageFileGroupId);
|
|
4352
|
+
if (referencesStorageFileGroup) {
|
|
4353
|
+
const fileAccessor = storageAccessor.file(storageFile);
|
|
4354
|
+
const metadata = await fileAccessor.getMetadata().catch(() => null);
|
|
4355
|
+
if (metadata) {
|
|
4356
|
+
const fileSlashPathDetails = util.slashPathDetails(metadata.name);
|
|
4357
|
+
const storageFileGroupEmbeddedFile = embeddedFilesMap.get(storageFile.id);
|
|
4358
|
+
const { n: embeddedFileNameOverride } = storageFileGroupEmbeddedFile;
|
|
4359
|
+
const nameFromFactory = await zipFileDisplayNameFunction({ metadata, fileAccessor, storageFile, storageFileDocument, storageFileGroupEmbeddedFile });
|
|
4360
|
+
let untypedName = nameFromFactory || storageFileDisplayName || embeddedFileNameOverride || fileSlashPathDetails.fileName;
|
|
4361
|
+
let extension;
|
|
4362
|
+
if (fileSlashPathDetails.typedFileExtension) {
|
|
4363
|
+
extension = fileSlashPathDetails.typedFileExtension;
|
|
4364
|
+
}
|
|
4365
|
+
else if (metadata.contentType) {
|
|
4366
|
+
extension = util.documentFileExtensionForMimeType(metadata.contentType);
|
|
4367
|
+
}
|
|
4368
|
+
// set the default name if still unset
|
|
4369
|
+
untypedName = untypedName || `sf_${storageFile.id}`;
|
|
4370
|
+
const name = extension ? `${untypedName}.${extension}` : untypedName;
|
|
4371
|
+
const fileStream = fileAccessor.getStream();
|
|
4372
|
+
await util.useCallback((x) => {
|
|
4373
|
+
// append the file to the archive
|
|
4374
|
+
newArchive.append(fileStream, {
|
|
4375
|
+
name
|
|
4376
|
+
});
|
|
4377
|
+
// if the stream errors, call back
|
|
4378
|
+
fileStream.on('error', (e) => x(e));
|
|
4379
|
+
// when the stream finishes, call back
|
|
4380
|
+
fileStream.on('finish', () => x());
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
else {
|
|
4384
|
+
flagCleanFileAssociations = true;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
else {
|
|
4388
|
+
flagCleanFileAssociations = true;
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
}, {
|
|
4392
|
+
maxParallelTasks: maxNumberOfFilesToZipInParallel
|
|
4393
|
+
});
|
|
4394
|
+
const finishedAt = new Date();
|
|
4395
|
+
// create the info.json file
|
|
4396
|
+
if (appendZipInfoJson) {
|
|
4397
|
+
const infoJson = await configureZipInfoJson({
|
|
4398
|
+
sfg: storageFileGroupId,
|
|
4399
|
+
sf: storageFileIdsToZip,
|
|
4400
|
+
s: startedAt.toISOString(),
|
|
4401
|
+
f: finishedAt.toISOString()
|
|
4402
|
+
});
|
|
4403
|
+
let infoJsonString;
|
|
4404
|
+
try {
|
|
4405
|
+
infoJsonString = JSON.stringify(infoJson);
|
|
4406
|
+
}
|
|
4407
|
+
catch (e) {
|
|
4408
|
+
console.error('storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(): Failed to convert the info json to a string. Check your custom configureInfoJson() function.', e);
|
|
4409
|
+
}
|
|
4410
|
+
if (infoJsonString) {
|
|
4411
|
+
newArchive.append(infoJsonString, {
|
|
4412
|
+
name: firebase.STORAGE_FILE_GROUP_ZIP_INFO_JSON_FILE_NAME
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
// perform any other tasks using the zip archive
|
|
4417
|
+
if (finalizeZipArchive) {
|
|
4418
|
+
await finalizeZipArchive({
|
|
4419
|
+
input,
|
|
4420
|
+
storageFileGroup,
|
|
4421
|
+
archive: newArchive
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
// finalize the archive
|
|
4425
|
+
await newArchive.finalize();
|
|
4426
|
+
// update the StorageFileGroup
|
|
4427
|
+
await storageFileGroupDocument.update({
|
|
4428
|
+
zat: finishedAt,
|
|
4429
|
+
c: flagCleanFileAssociations
|
|
4430
|
+
});
|
|
4431
|
+
// schedule/run the cleanup task
|
|
4432
|
+
result = firebase.notificationSubtaskComplete({
|
|
4433
|
+
canRunNextCheckpoint: true
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
else {
|
|
4437
|
+
// uploadStream is not available for some reason? Should never occur.
|
|
4438
|
+
console.warn('storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(): uploadStream is not available for some reason while creating a new zip.');
|
|
4439
|
+
result = firebase.notificationTaskDelayRetry(util.MS_IN_HOUR);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
else {
|
|
4443
|
+
// storage file group no longer exists. Flag the StorageFile for deletion.
|
|
4444
|
+
result = await flagStorageFileForDeletion();
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
else {
|
|
4448
|
+
// improperly configured StorageFile for this type. Flag the StorageFile for deletion.
|
|
4449
|
+
result = await flagStorageFileForDeletion();
|
|
4450
|
+
}
|
|
4451
|
+
return result;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
]
|
|
4455
|
+
};
|
|
4456
|
+
return storageFileGroupZipProcessorConfig;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function storageFileInitializeFromUploadServiceInitializerResultPermanentFailure(error, createdFile) {
|
|
4460
|
+
return {
|
|
4461
|
+
error,
|
|
4462
|
+
permanentFailure: true,
|
|
4463
|
+
createdFile
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* A basic StorageFileInitializeFromUploadService implementation.
|
|
4468
|
+
*/
|
|
4469
|
+
function storageFileInitializeFromUploadService(config) {
|
|
4470
|
+
const { storageService, storageFileCollection, initializer: inputInitializers, determiner: inputDeterminers, validate, checkFileIsAllowedToBeInitialized: inputCheckFileIsAllowedToBeInitialized, requireStorageFileRelatedFileMetadataBeSet } = config;
|
|
4471
|
+
const allDeterminers = [];
|
|
4472
|
+
const initializers = {};
|
|
4473
|
+
const detailsAccessorFactory = firebase.storedFileReaderFactory();
|
|
4474
|
+
if (inputDeterminers) {
|
|
4475
|
+
util.pushItemOrArrayItemsIntoArray(allDeterminers, inputDeterminers);
|
|
4476
|
+
}
|
|
4477
|
+
// iterate initializers
|
|
4478
|
+
inputInitializers.forEach((initializer) => {
|
|
4479
|
+
const { type: inputTypes, determiner: inputDeterminer } = initializer;
|
|
4480
|
+
const types = util.asArray(inputTypes);
|
|
4481
|
+
types.forEach((type) => {
|
|
4482
|
+
initializers[type] = initializer;
|
|
4483
|
+
});
|
|
4484
|
+
if (inputDeterminer) {
|
|
4485
|
+
const wrappedDeterminer = firebase.limitUploadFileTypeDeterminer(inputDeterminer, types);
|
|
4486
|
+
allDeterminers.push(wrappedDeterminer);
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
const determiner = firebase.combineUploadFileTypeDeterminers({
|
|
4490
|
+
determiners: allDeterminers,
|
|
4491
|
+
...{
|
|
4492
|
+
completeSearchOnFirstMatch: true,
|
|
4493
|
+
...config.combineDeterminersConfig
|
|
4494
|
+
}
|
|
4495
|
+
});
|
|
4496
|
+
// validate initializers
|
|
4497
|
+
if (validate) {
|
|
4498
|
+
const allInitializerTypes = Object.keys(initializers);
|
|
4499
|
+
const allDeterminerTypes = new Set(determiner.getPossibleFileTypes());
|
|
4500
|
+
// all initializer types should have a corresponding determiner
|
|
4501
|
+
allInitializerTypes.forEach((type) => {
|
|
4502
|
+
if (!allDeterminerTypes.has(type)) {
|
|
4503
|
+
throw new Error(`Initializer type ${type} does not have a corresponding determiner.`);
|
|
4504
|
+
}
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
async function determineUploadFileType(input) {
|
|
4508
|
+
const { file } = input;
|
|
4509
|
+
const fileDetailsAccessor = detailsAccessorFactory(file);
|
|
4510
|
+
return determiner.determine(fileDetailsAccessor);
|
|
4511
|
+
}
|
|
4512
|
+
return {
|
|
4513
|
+
checkFileIsAllowedToBeInitialized: inputCheckFileIsAllowedToBeInitialized ?? util.asDecisionFunction(true),
|
|
4514
|
+
determineUploadFileType,
|
|
4515
|
+
initializeFromUpload: async (input) => {
|
|
4516
|
+
const determinerResult = await determineUploadFileType(input);
|
|
4517
|
+
let resultType;
|
|
4518
|
+
let createdFilePath;
|
|
4519
|
+
let storageFileDocument;
|
|
4520
|
+
let processorError;
|
|
4521
|
+
let previousStorageFilesFlaggedForDeletion;
|
|
4522
|
+
if (determinerResult) {
|
|
4523
|
+
const { input: fileDetailsAccessor } = determinerResult;
|
|
4524
|
+
resultType = 'success';
|
|
4525
|
+
const initializer = initializers[determinerResult.type];
|
|
4526
|
+
if (initializer) {
|
|
4527
|
+
try {
|
|
4528
|
+
const initializerResult = await initializer.initialize({ determinerResult, fileDetailsAccessor });
|
|
4529
|
+
if (initializerResult.error) {
|
|
4530
|
+
const { error, permanentFailure, createdFile } = initializerResult;
|
|
4531
|
+
processorError = error;
|
|
4532
|
+
if (permanentFailure) {
|
|
4533
|
+
resultType = 'permanent_initializer_failure';
|
|
4534
|
+
}
|
|
4535
|
+
else {
|
|
4536
|
+
resultType = 'initializer_error';
|
|
4537
|
+
}
|
|
4538
|
+
// delete the created file
|
|
4539
|
+
if (createdFile != null) {
|
|
4540
|
+
await storageService
|
|
4541
|
+
.file(createdFile.storagePath)
|
|
4542
|
+
.delete()
|
|
4543
|
+
.catch(() => 0);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
else {
|
|
4547
|
+
let flagPreviousForDelete;
|
|
4548
|
+
if (initializerResult.createStorageFileResult) {
|
|
4549
|
+
const { createStorageFileResult, flagPreviousForDelete: flagPreviousForDeleteResult } = initializerResult;
|
|
4550
|
+
createdFilePath = firebase.copyStoragePath(createStorageFileResult.storageFile);
|
|
4551
|
+
storageFileDocument = createStorageFileResult.storageFileDocument;
|
|
4552
|
+
if (flagPreviousForDeleteResult) {
|
|
4553
|
+
if (typeof flagPreviousForDeleteResult === 'object') {
|
|
4554
|
+
flagPreviousForDelete = flagPreviousForDeleteResult;
|
|
4555
|
+
}
|
|
4556
|
+
else {
|
|
4557
|
+
const { p, pg, u } = createStorageFileResult.storageFile;
|
|
4558
|
+
if (!p || !u) {
|
|
4559
|
+
throw new Error('initializeFromUpload(): flagPreviousForDelete=true requires that the created StorageFile have a purpose (p) and user (u).');
|
|
4560
|
+
}
|
|
4561
|
+
flagPreviousForDelete = {
|
|
4562
|
+
purpose: p,
|
|
4563
|
+
purposeSubgroup: pg,
|
|
4564
|
+
user: u
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
else {
|
|
4570
|
+
createdFilePath = firebase.copyStoragePath(initializerResult.createdFile.storagePath);
|
|
4571
|
+
storageFileDocument = initializerResult.storageFileDocument;
|
|
4572
|
+
flagPreviousForDelete = initializerResult.flagPreviousForDelete;
|
|
4573
|
+
}
|
|
4574
|
+
// sanitize the returned value, incase the result comes from a transaction
|
|
4575
|
+
if (storageFileDocument) {
|
|
4576
|
+
storageFileDocument = storageFileCollection.documentAccessor().loadDocumentFrom(storageFileDocument);
|
|
4577
|
+
}
|
|
4578
|
+
// set the metadata on the associated file
|
|
4579
|
+
try {
|
|
4580
|
+
const createdFile = storageService.file(createdFilePath);
|
|
4581
|
+
const fileMetadata = await createdFile.getMetadata();
|
|
4582
|
+
await createdFile.setMetadata({
|
|
4583
|
+
customMetadata: {
|
|
4584
|
+
...fileMetadata.customMetadata,
|
|
4585
|
+
[firebase.STORAGEFILE_RELATED_FILE_METADATA_KEY]: storageFileDocument.id
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
catch (e) {
|
|
4590
|
+
// failed to set the metadata. It isn't strictly necessary, so don't throw an error unless configured to always throw an error
|
|
4591
|
+
if (requireStorageFileRelatedFileMetadataBeSet) {
|
|
4592
|
+
// mark the created item for delete
|
|
4593
|
+
await storageFileDocument.update(markStorageFileForDeleteTemplate());
|
|
4594
|
+
// throw the exception
|
|
4595
|
+
throw e;
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
// if flagPreviousForDelete is set, flag the previous storage files for deletion
|
|
4599
|
+
if (flagPreviousForDelete) {
|
|
4600
|
+
const flagForDeleteResult = await queryAndFlagStorageFilesForDelete({
|
|
4601
|
+
storageFileCollection,
|
|
4602
|
+
constraints: firebase.storageFilePurposeAndUserQuery(flagPreviousForDelete),
|
|
4603
|
+
skipDeleteForKeys: [storageFileDocument.key]
|
|
4604
|
+
});
|
|
4605
|
+
previousStorageFilesFlaggedForDeletion = flagForDeleteResult.queuedForDeleteCount;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
catch (e) {
|
|
4610
|
+
resultType = 'initializer_error';
|
|
4611
|
+
processorError = e;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
else {
|
|
4615
|
+
resultType = 'no_initializer_configured';
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
else {
|
|
4619
|
+
resultType = 'no_determiner_match';
|
|
4620
|
+
}
|
|
4621
|
+
const result = {
|
|
4622
|
+
resultType,
|
|
4623
|
+
storageFileDocument,
|
|
4624
|
+
initializationError: processorError,
|
|
4625
|
+
previousStorageFilesFlaggedForDeletion
|
|
4626
|
+
};
|
|
4627
|
+
return result;
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
exports.BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN;
|
|
4633
|
+
exports.BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN;
|
|
4634
|
+
exports.KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS;
|
|
4635
|
+
exports.KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
|
|
4636
|
+
exports.MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST = MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST;
|
|
4637
|
+
exports.MAKE_TEMPLATE_FOR_NOTIFICATION_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = MAKE_TEMPLATE_FOR_NOTIFICATION_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE;
|
|
4638
|
+
exports.MAKE_TEMPLATE_FOR_STORAGEFILE_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = MAKE_TEMPLATE_FOR_STORAGEFILE_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE;
|
|
4639
|
+
exports.NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES = NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES;
|
|
4640
|
+
exports.NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN;
|
|
4641
|
+
exports.NOTIFICATION_MAX_SEND_ATTEMPTS = NOTIFICATION_MAX_SEND_ATTEMPTS;
|
|
4642
|
+
exports.NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN;
|
|
4643
|
+
exports.NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES = NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES;
|
|
4644
|
+
exports.NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS = NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS;
|
|
4645
|
+
exports.NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS = NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS;
|
|
4646
|
+
exports.NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS = NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS;
|
|
4647
|
+
exports.NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN = NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN;
|
|
4648
|
+
exports.NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN = NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN;
|
|
4649
|
+
exports.NotificationExpediteService = NotificationExpediteService;
|
|
4650
|
+
exports.NotificationInitServerActions = NotificationInitServerActions;
|
|
4651
|
+
exports.NotificationSendService = NotificationSendService;
|
|
4652
|
+
exports.NotificationServerActions = NotificationServerActions;
|
|
4653
|
+
exports.NotificationTaskService = NotificationTaskService;
|
|
4654
|
+
exports.NotificationTaskSubTaskMissingRequiredDataTermination = NotificationTaskSubTaskMissingRequiredDataTermination;
|
|
4655
|
+
exports.SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD = SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD;
|
|
4656
|
+
exports.STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN;
|
|
4657
|
+
exports.STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN;
|
|
4658
|
+
exports.StorageFileInitServerActions = StorageFileInitServerActions;
|
|
4659
|
+
exports.StorageFileInitializeFromUploadService = StorageFileInitializeFromUploadService;
|
|
4660
|
+
exports.StorageFileServerActions = StorageFileServerActions;
|
|
4661
|
+
exports.UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS;
|
|
4662
|
+
exports.UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY = UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY;
|
|
4663
|
+
exports.UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS;
|
|
4664
|
+
exports.UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
|
|
4665
|
+
exports._initializeStorageFileFromUploadFileFactory = _initializeStorageFileFromUploadFileFactory;
|
|
4666
|
+
exports._processStorageFileInTransactionFactory = _processStorageFileInTransactionFactory;
|
|
4667
|
+
exports._syncStorageFileWithGroupsInTransactionFactory = _syncStorageFileWithGroupsInTransactionFactory;
|
|
4668
|
+
exports.allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors = allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors;
|
|
4669
|
+
exports.appNotificationModuleMetadata = appNotificationModuleMetadata;
|
|
4670
|
+
exports.appStorageFileModuleMetadata = appStorageFileModuleMetadata;
|
|
4671
|
+
exports.cleanupSentNotificationsFactory = cleanupSentNotificationsFactory;
|
|
4672
|
+
exports.createNotificationBoxFactory = createNotificationBoxFactory;
|
|
4673
|
+
exports.createNotificationBoxInTransactionFactory = createNotificationBoxInTransactionFactory;
|
|
4674
|
+
exports.createNotificationIdRequiredError = createNotificationIdRequiredError;
|
|
4675
|
+
exports.createNotificationSummaryFactory = createNotificationSummaryFactory;
|
|
4676
|
+
exports.createNotificationUserFactory = createNotificationUserFactory;
|
|
4677
|
+
exports.createOrRunUniqueNotificationDocument = createOrRunUniqueNotificationDocument;
|
|
4678
|
+
exports.createStorageFileFactory = createStorageFileFactory;
|
|
4679
|
+
exports.createStorageFileGroupFactory = createStorageFileGroupFactory;
|
|
4680
|
+
exports.createStorageFileGroupInTransactionFactory = createStorageFileGroupInTransactionFactory;
|
|
4681
|
+
exports.createStorageFileGroupInputError = createStorageFileGroupInputError;
|
|
4682
|
+
exports.deleteAllQueuedStorageFilesFactory = deleteAllQueuedStorageFilesFactory;
|
|
4683
|
+
exports.deleteStorageFileFactory = deleteStorageFileFactory;
|
|
4684
|
+
exports.downloadStorageFileFactory = downloadStorageFileFactory;
|
|
4685
|
+
exports.expandNotificationRecipients = expandNotificationRecipients;
|
|
4686
|
+
exports.exportMutableNotificationExpediteService = exportMutableNotificationExpediteService;
|
|
4687
|
+
exports.firestoreNotificationSummarySendService = firestoreNotificationSummarySendService;
|
|
4688
|
+
exports.ignoreSendNotificationTextSendService = ignoreSendNotificationTextSendService;
|
|
4689
|
+
exports.initializeAllApplicableNotificationBoxesFactory = initializeAllApplicableNotificationBoxesFactory;
|
|
4690
|
+
exports.initializeAllApplicableNotificationSummariesFactory = initializeAllApplicableNotificationSummariesFactory;
|
|
4691
|
+
exports.initializeAllApplicableStorageFileGroupsFactory = initializeAllApplicableStorageFileGroupsFactory;
|
|
4692
|
+
exports.initializeAllStorageFilesFromUploadsFactory = initializeAllStorageFilesFromUploadsFactory;
|
|
4693
|
+
exports.initializeNotificationBoxFactory = initializeNotificationBoxFactory;
|
|
4694
|
+
exports.initializeNotificationBoxInTransactionFactory = initializeNotificationBoxInTransactionFactory;
|
|
4695
|
+
exports.initializeNotificationModelInTransaction = initializeNotificationModelInTransaction;
|
|
4696
|
+
exports.initializeNotificationSummaryFactory = initializeNotificationSummaryFactory;
|
|
4697
|
+
exports.initializeNotificationSummaryInTransactionFactory = initializeNotificationSummaryInTransactionFactory;
|
|
4698
|
+
exports.initializeStorageFileFromUploadFactory = initializeStorageFileFromUploadFactory;
|
|
4699
|
+
exports.initializeStorageFileGroupFactory = initializeStorageFileGroupFactory;
|
|
4700
|
+
exports.initializeStorageFileGroupInTransactionFactory = initializeStorageFileGroupInTransactionFactory;
|
|
4701
|
+
exports.initializeStorageFileModelInTransaction = initializeStorageFileModelInTransaction;
|
|
4702
|
+
exports.mailgunNotificationEmailSendService = mailgunNotificationEmailSendService;
|
|
4703
|
+
exports.makeNewNotificationSummaryTemplate = makeNewNotificationSummaryTemplate;
|
|
4704
|
+
exports.markStorageFileForDeleteTemplate = markStorageFileForDeleteTemplate;
|
|
4705
|
+
exports.notificationBoxDoesNotExist = notificationBoxDoesNotExist;
|
|
4706
|
+
exports.notificationBoxExclusionTargetInvalidError = notificationBoxExclusionTargetInvalidError;
|
|
4707
|
+
exports.notificationBoxExistsForModelError = notificationBoxExistsForModelError;
|
|
4708
|
+
exports.notificationBoxRecipientDoesNotExistsError = notificationBoxRecipientDoesNotExistsError;
|
|
4709
|
+
exports.notificationBoxUnregistredModelTypeInitializationError = notificationBoxUnregistredModelTypeInitializationError;
|
|
4710
|
+
exports.notificationExpediteServiceInstance = notificationExpediteServiceInstance;
|
|
4711
|
+
exports.notificationInitServerActions = notificationInitServerActions;
|
|
4712
|
+
exports.notificationInitServerActionsFactory = notificationInitServerActionsFactory;
|
|
4713
|
+
exports.notificationModelAlreadyInitializedError = notificationModelAlreadyInitializedError;
|
|
4714
|
+
exports.notificationServerActions = notificationServerActions;
|
|
4715
|
+
exports.notificationServerActionsContextFactory = notificationServerActionsContextFactory;
|
|
4716
|
+
exports.notificationServerActionsFactory = notificationServerActionsFactory;
|
|
4717
|
+
exports.notificationTaskService = notificationTaskService;
|
|
4718
|
+
exports.notificationTaskSubTaskMissingRequiredDataTermination = notificationTaskSubTaskMissingRequiredDataTermination;
|
|
4719
|
+
exports.notificationTaskSubtaskNotificationTaskHandlerFactory = notificationTaskSubtaskNotificationTaskHandlerFactory;
|
|
4720
|
+
exports.notificationTemplateServiceInstance = notificationTemplateServiceInstance;
|
|
4721
|
+
exports.notificationUserBlockedFromBeingAddedToRecipientsError = notificationUserBlockedFromBeingAddedToRecipientsError;
|
|
4722
|
+
exports.notificationUserInvalidUidForCreateError = notificationUserInvalidUidForCreateError;
|
|
4723
|
+
exports.notificationUserLockedConfigFromBeingUpdatedError = notificationUserLockedConfigFromBeingUpdatedError;
|
|
4724
|
+
exports.processAllQueuedStorageFilesFactory = processAllQueuedStorageFilesFactory;
|
|
4725
|
+
exports.processStorageFileFactory = processStorageFileFactory;
|
|
4726
|
+
exports.provideMutableNotificationExpediteService = provideMutableNotificationExpediteService;
|
|
4727
|
+
exports.queryAndFlagStorageFilesForDelete = queryAndFlagStorageFilesForDelete;
|
|
4728
|
+
exports.regenerateAllFlaggedStorageFileGroupsContentFactory = regenerateAllFlaggedStorageFileGroupsContentFactory;
|
|
4729
|
+
exports.regenerateStorageFileGroupContentFactory = regenerateStorageFileGroupContentFactory;
|
|
4730
|
+
exports.resyncAllNotificationUsersFactory = resyncAllNotificationUsersFactory;
|
|
4731
|
+
exports.resyncNotificationUserFactory = resyncNotificationUserFactory;
|
|
4732
|
+
exports.sendNotificationFactory = sendNotificationFactory;
|
|
4733
|
+
exports.sendQueuedNotificationsFactory = sendQueuedNotificationsFactory;
|
|
4734
|
+
exports.storageFileAlreadyProcessedError = storageFileAlreadyProcessedError;
|
|
4735
|
+
exports.storageFileCannotBeDeletedYetError = storageFileCannotBeDeletedYetError;
|
|
4736
|
+
exports.storageFileGroupQueuedForInitializationError = storageFileGroupQueuedForInitializationError;
|
|
4737
|
+
exports.storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor = storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor;
|
|
4738
|
+
exports.storageFileInitServerActions = storageFileInitServerActions;
|
|
4739
|
+
exports.storageFileInitServerActionsFactory = storageFileInitServerActionsFactory;
|
|
4740
|
+
exports.storageFileInitializeFromUploadService = storageFileInitializeFromUploadService;
|
|
4741
|
+
exports.storageFileInitializeFromUploadServiceInitializerResultPermanentFailure = storageFileInitializeFromUploadServiceInitializerResultPermanentFailure;
|
|
4742
|
+
exports.storageFileModelAlreadyInitializedError = storageFileModelAlreadyInitializedError;
|
|
4743
|
+
exports.storageFileNotFlaggedForDeletionError = storageFileNotFlaggedForDeletionError;
|
|
4744
|
+
exports.storageFileNotFlaggedForGroupsSyncError = storageFileNotFlaggedForGroupsSyncError;
|
|
4745
|
+
exports.storageFileProcessingNotAllowedForInvalidStateError = storageFileProcessingNotAllowedForInvalidStateError;
|
|
4746
|
+
exports.storageFileProcessingNotAvailableForTypeError = storageFileProcessingNotAvailableForTypeError;
|
|
4747
|
+
exports.storageFileProcessingNotQueuedForProcessingError = storageFileProcessingNotQueuedForProcessingError;
|
|
4748
|
+
exports.storageFileProcessingNotificationTaskHandler = storageFileProcessingNotificationTaskHandler;
|
|
4749
|
+
exports.storageFileProcessingNotificationTaskHandlerDefaultCleanup = storageFileProcessingNotificationTaskHandlerDefaultCleanup;
|
|
4750
|
+
exports.storageFileServerActions = storageFileServerActions;
|
|
4751
|
+
exports.storageFileServerActionsContextFactory = storageFileServerActionsContextFactory;
|
|
4752
|
+
exports.storageFileServerActionsFactory = storageFileServerActionsFactory;
|
|
4753
|
+
exports.syncAllFlaggedStorageFilesWithGroupsFactory = syncAllFlaggedStorageFilesWithGroupsFactory;
|
|
4754
|
+
exports.syncStorageFileWithGroupsFactory = syncStorageFileWithGroupsFactory;
|
|
4755
|
+
exports.updateNotificationBoxFactory = updateNotificationBoxFactory;
|
|
4756
|
+
exports.updateNotificationBoxRecipientExclusionInTransactionFactory = updateNotificationBoxRecipientExclusionInTransactionFactory;
|
|
4757
|
+
exports.updateNotificationBoxRecipientFactory = updateNotificationBoxRecipientFactory;
|
|
4758
|
+
exports.updateNotificationBoxRecipientInTransactionFactory = updateNotificationBoxRecipientInTransactionFactory;
|
|
4759
|
+
exports.updateNotificationSummaryFactory = updateNotificationSummaryFactory;
|
|
4760
|
+
exports.updateNotificationUserFactory = updateNotificationUserFactory;
|
|
4761
|
+
exports.updateNotificationUserNotificationBoxRecipientConfig = updateNotificationUserNotificationBoxRecipientConfig;
|
|
4762
|
+
exports.updateStorageFileFactory = updateStorageFileFactory;
|
|
4763
|
+
exports.updateStorageFileGroupFactory = updateStorageFileGroupFactory;
|
|
4764
|
+
exports.uploadedFileDoesNotExistError = uploadedFileDoesNotExistError;
|
|
4765
|
+
exports.uploadedFileInitializationDiscardedError = uploadedFileInitializationDiscardedError;
|
|
4766
|
+
exports.uploadedFileInitializationFailedError = uploadedFileInitializationFailedError;
|
|
4767
|
+
exports.uploadedFileIsNotAllowedToBeInitializedError = uploadedFileIsNotAllowedToBeInitializedError;
|