@stamhoofd/models 2.94.0 → 2.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/helpers/EmailBuilder.d.ts +27 -1
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +270 -25
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
- package/dist/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
- package/dist/src/models/Email.d.ts +3 -1
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +143 -4
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/EmailRecipient.d.ts +7 -1
- package/dist/src/models/EmailRecipient.d.ts.map +1 -1
- package/dist/src/models/EmailRecipient.js +28 -2
- package/dist/src/models/EmailRecipient.js.map +1 -1
- package/dist/src/models/Member.d.ts +1 -1
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +3 -3
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/RegistrationPeriod.d.ts +1 -0
- package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
- package/dist/src/models/RegistrationPeriod.js +4 -0
- package/dist/src/models/RegistrationPeriod.js.map +1 -1
- package/dist/src/models/User.d.ts.map +1 -1
- package/dist/src/models/User.js +5 -1
- package/dist/src/models/User.js.map +1 -1
- package/package.json +2 -2
- package/src/helpers/EmailBuilder.ts +282 -34
- package/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
- package/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
- package/src/models/Email.ts +169 -7
- package/src/models/EmailRecipient.ts +32 -3
- package/src/models/Member.ts +3 -3
- package/src/models/RegistrationPeriod.ts +3 -0
- package/src/models/User.ts +7 -1
package/src/models/Email.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { column } from '@simonbackx/simple-database';
|
|
2
|
-
import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse, SortItemDirection, StamhoofdFilter,
|
|
2
|
+
import { User as UserStruct, EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, EmailWithRecipients, getExampleRecipient, isSoftEmailRecipientError, LimitedFilteredRequest, PaginatedResponse, Replacement, SortItemDirection, StamhoofdFilter, BaseOrganization } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
@@ -8,10 +8,11 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
8
8
|
import { Email as EmailClass, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
9
9
|
import { isAbortedError, QueueHandler, QueueHandlerOptions } from '@stamhoofd/queues';
|
|
10
10
|
import { QueryableModel, readDynamicSQLExpression, SQL, SQLAlias, SQLCalculation, SQLCount, SQLPlusSign, SQLSelectAs, SQLWhereSign } from '@stamhoofd/sql';
|
|
11
|
-
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
|
|
11
|
+
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder, mergeReplacementsIfEqual, removeUnusedReplacements, stripRecipientReplacementsForWebDisplay, stripSensitiveRecipientReplacements } from '../helpers/EmailBuilder';
|
|
12
12
|
import { EmailRecipient } from './EmailRecipient';
|
|
13
13
|
import { EmailTemplate } from './EmailTemplate';
|
|
14
14
|
import { Organization } from './Organization';
|
|
15
|
+
import { User } from './User';
|
|
15
16
|
|
|
16
17
|
function errorToSimpleErrors(e: unknown) {
|
|
17
18
|
if (isSimpleErrors(e)) {
|
|
@@ -758,6 +759,11 @@ export class Email extends QueryableModel {
|
|
|
758
759
|
continue;
|
|
759
760
|
}
|
|
760
761
|
|
|
762
|
+
if (recipient.duplicateOfRecipientId) {
|
|
763
|
+
skipped++;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
|
|
761
767
|
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
|
762
768
|
const promise = new Promise<void>((resolve) => {
|
|
763
769
|
promiseResolve = resolve;
|
|
@@ -988,12 +994,15 @@ export class Email extends QueryableModel {
|
|
|
988
994
|
|
|
989
995
|
abort.throwIfAborted();
|
|
990
996
|
|
|
997
|
+
console.log('Building recipients for email', id);
|
|
998
|
+
|
|
991
999
|
// If it is already creating -> something went wrong (e.g. server restart) and we can safely try again
|
|
992
1000
|
|
|
993
1001
|
upToDate.recipientsStatus = EmailRecipientsStatus.Creating;
|
|
994
1002
|
await upToDate.save();
|
|
995
1003
|
|
|
996
1004
|
const membersSet = new Set<string>();
|
|
1005
|
+
const emailsSet = new Set<string>();
|
|
997
1006
|
|
|
998
1007
|
let count = 0;
|
|
999
1008
|
let countWithoutEmail = 0;
|
|
@@ -1033,6 +1042,39 @@ export class Email extends QueryableModel {
|
|
|
1033
1042
|
continue;
|
|
1034
1043
|
}
|
|
1035
1044
|
|
|
1045
|
+
item.replacements = removeUnusedReplacements(upToDate.html ?? '', item.replacements);
|
|
1046
|
+
|
|
1047
|
+
let duplicateOfRecipientId: string | null = null;
|
|
1048
|
+
if (item.email && emailsSet.has(item.email)) {
|
|
1049
|
+
console.log('Found duplicate email recipient', item.email);
|
|
1050
|
+
|
|
1051
|
+
// Try to merge
|
|
1052
|
+
const existing = await EmailRecipient.select()
|
|
1053
|
+
.where('emailId', upToDate.id)
|
|
1054
|
+
.where('email', item.email)
|
|
1055
|
+
.fetch();
|
|
1056
|
+
|
|
1057
|
+
for (const other of existing) {
|
|
1058
|
+
const merged = mergeReplacementsIfEqual(other.replacements, item.replacements);
|
|
1059
|
+
if (merged !== false) {
|
|
1060
|
+
console.log('Found duplicate email recipient', item.email, other.id);
|
|
1061
|
+
duplicateOfRecipientId = other.id;
|
|
1062
|
+
|
|
1063
|
+
other.replacements = merged;
|
|
1064
|
+
other.firstName = other.firstName || item.firstName;
|
|
1065
|
+
other.lastName = other.lastName || item.lastName;
|
|
1066
|
+
await other.save();
|
|
1067
|
+
|
|
1068
|
+
item.replacements = merged;
|
|
1069
|
+
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
console.log('Could not merge duplicate email recipient', item.email, other.id, 'keeping both', other.replacements, item.replacements);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1036
1078
|
const recipient = new EmailRecipient();
|
|
1037
1079
|
recipient.emailType = upToDate.emailType;
|
|
1038
1080
|
recipient.objectId = item.objectId;
|
|
@@ -1044,6 +1086,7 @@ export class Email extends QueryableModel {
|
|
|
1044
1086
|
recipient.memberId = item.memberId ?? null;
|
|
1045
1087
|
recipient.userId = item.userId ?? null;
|
|
1046
1088
|
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1089
|
+
recipient.duplicateOfRecipientId = duplicateOfRecipientId;
|
|
1047
1090
|
|
|
1048
1091
|
await recipient.save();
|
|
1049
1092
|
|
|
@@ -1051,7 +1094,11 @@ export class Email extends QueryableModel {
|
|
|
1051
1094
|
membersSet.add(recipient.memberId);
|
|
1052
1095
|
}
|
|
1053
1096
|
|
|
1054
|
-
if (
|
|
1097
|
+
if (recipient.email) {
|
|
1098
|
+
emailsSet.add(recipient.email);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!recipient.email || duplicateOfRecipientId) {
|
|
1055
1102
|
countWithoutEmail += 1;
|
|
1056
1103
|
}
|
|
1057
1104
|
else {
|
|
@@ -1125,13 +1172,21 @@ export class Email extends QueryableModel {
|
|
|
1125
1172
|
|
|
1126
1173
|
for (const item of response.results) {
|
|
1127
1174
|
const recipient = new EmailRecipient();
|
|
1175
|
+
recipient.emailType = upToDate.emailType;
|
|
1128
1176
|
recipient.emailId = upToDate.id;
|
|
1177
|
+
recipient.objectId = item.objectId;
|
|
1129
1178
|
recipient.email = item.email;
|
|
1130
1179
|
recipient.firstName = item.firstName;
|
|
1131
1180
|
recipient.lastName = item.lastName;
|
|
1132
1181
|
recipient.replacements = item.replacements;
|
|
1182
|
+
recipient.memberId = item.memberId ?? null;
|
|
1183
|
+
recipient.userId = item.userId ?? null;
|
|
1184
|
+
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1133
1185
|
await recipient.save();
|
|
1134
|
-
|
|
1186
|
+
|
|
1187
|
+
if (recipient.email || recipient.userId) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1135
1190
|
}
|
|
1136
1191
|
|
|
1137
1192
|
request = null;
|
|
@@ -1154,12 +1209,13 @@ export class Email extends QueryableModel {
|
|
|
1154
1209
|
async getPreviewStructure() {
|
|
1155
1210
|
const emailRecipient = await EmailRecipient.select()
|
|
1156
1211
|
.where('emailId', this.id)
|
|
1212
|
+
.where('email', '!=', null)
|
|
1157
1213
|
.first(false);
|
|
1158
1214
|
|
|
1159
1215
|
let recipientRow: EmailRecipientStruct | undefined;
|
|
1160
1216
|
|
|
1161
1217
|
if (emailRecipient) {
|
|
1162
|
-
recipientRow = emailRecipient.getStructure();
|
|
1218
|
+
recipientRow = await emailRecipient.getStructure();
|
|
1163
1219
|
}
|
|
1164
1220
|
|
|
1165
1221
|
if (!recipientRow) {
|
|
@@ -1167,18 +1223,124 @@ export class Email extends QueryableModel {
|
|
|
1167
1223
|
}
|
|
1168
1224
|
|
|
1169
1225
|
const virtualRecipient = recipientRow.getRecipient();
|
|
1226
|
+
const organization = this.organizationId ? (await Organization.getByID(this.organizationId))! : null;
|
|
1170
1227
|
|
|
1171
1228
|
await fillRecipientReplacements(virtualRecipient, {
|
|
1172
|
-
organization
|
|
1229
|
+
organization,
|
|
1173
1230
|
from: this.getFromAddress(),
|
|
1174
1231
|
replyTo: null,
|
|
1232
|
+
forPreview: true,
|
|
1233
|
+
forceRefresh: !this.sentAt,
|
|
1175
1234
|
});
|
|
1176
|
-
|
|
1177
1235
|
recipientRow.replacements = virtualRecipient.replacements;
|
|
1178
1236
|
|
|
1237
|
+
let user: UserStruct | null = null;
|
|
1238
|
+
if (this.userId) {
|
|
1239
|
+
const u = await User.getByID(this.userId);
|
|
1240
|
+
if (u) {
|
|
1241
|
+
user = u.getStructure();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
let organizationStruct: BaseOrganization | null = null;
|
|
1246
|
+
if (organization) {
|
|
1247
|
+
organizationStruct = organization.getBaseStructure();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1179
1250
|
return EmailPreview.create({
|
|
1180
1251
|
...this,
|
|
1252
|
+
user,
|
|
1253
|
+
organization: organizationStruct,
|
|
1181
1254
|
exampleRecipient: recipientRow,
|
|
1182
1255
|
});
|
|
1183
1256
|
}
|
|
1257
|
+
|
|
1258
|
+
async getStructureForUser(user: User, memberIds: string[]) {
|
|
1259
|
+
const emailRecipients = await EmailRecipient.select()
|
|
1260
|
+
.where('emailId', this.id)
|
|
1261
|
+
.where('memberId', memberIds)
|
|
1262
|
+
.fetch();
|
|
1263
|
+
const organization = this.organizationId ? (await Organization.getByID(this.organizationId))! : null;
|
|
1264
|
+
|
|
1265
|
+
const recipientsMap: Map<string, EmailRecipient> = new Map();
|
|
1266
|
+
for (const memberId of memberIds) {
|
|
1267
|
+
const preferred = emailRecipients.find(e => e.memberId === memberId && (e.userId === user.id || e.email === user.email));
|
|
1268
|
+
if (preferred) {
|
|
1269
|
+
recipientsMap.set(preferred.duplicateOfRecipientId ?? preferred.id, preferred);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const byMember = emailRecipients.find(e => e.memberId === memberId && e.userId === null && e.email === null);
|
|
1274
|
+
if (byMember) {
|
|
1275
|
+
recipientsMap.set(byMember.duplicateOfRecipientId ?? byMember.id, byMember);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const anyData = emailRecipients.find(e => e.memberId === memberId);
|
|
1279
|
+
if (anyData) {
|
|
1280
|
+
recipientsMap.set(anyData.duplicateOfRecipientId ?? anyData.id, anyData);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Remove duplicates that are marked as the same recipient
|
|
1286
|
+
const cleanedRecipients: EmailRecipient[] = [...recipientsMap.values()];
|
|
1287
|
+
const structures = await EmailRecipient.getStructures(cleanedRecipients);
|
|
1288
|
+
|
|
1289
|
+
const virtualRecipients = structures.map((struct) => {
|
|
1290
|
+
const recipient = struct.getRecipient();
|
|
1291
|
+
|
|
1292
|
+
return {
|
|
1293
|
+
struct,
|
|
1294
|
+
recipient,
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
1297
|
+
for (const { struct, recipient } of virtualRecipients) {
|
|
1298
|
+
if (!(struct.userId === user.id || struct.email === user.email) && !((struct.userId === null && struct.email === null))) {
|
|
1299
|
+
stripSensitiveRecipientReplacements(recipient, {
|
|
1300
|
+
organization,
|
|
1301
|
+
willFill: true,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
recipient.email = user.email;
|
|
1306
|
+
recipient.userId = user.id;
|
|
1307
|
+
|
|
1308
|
+
// We always refresh the data when we display it on the web (so everything is up to date)
|
|
1309
|
+
await fillRecipientReplacements(recipient, {
|
|
1310
|
+
organization,
|
|
1311
|
+
from: this.getFromAddress(),
|
|
1312
|
+
replyTo: null,
|
|
1313
|
+
forPreview: false,
|
|
1314
|
+
forceRefresh: true,
|
|
1315
|
+
});
|
|
1316
|
+
stripRecipientReplacementsForWebDisplay(recipient, {
|
|
1317
|
+
organization,
|
|
1318
|
+
});
|
|
1319
|
+
struct.replacements = recipient.replacements;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let organizationStruct: BaseOrganization | null = null;
|
|
1323
|
+
if (organization) {
|
|
1324
|
+
organizationStruct = organization.getBaseStructure();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return EmailWithRecipients.create({
|
|
1328
|
+
...this,
|
|
1329
|
+
organization: organizationStruct,
|
|
1330
|
+
recipients: structures,
|
|
1331
|
+
|
|
1332
|
+
// Remove private-like data
|
|
1333
|
+
softBouncesCount: 0,
|
|
1334
|
+
failedCount: 0,
|
|
1335
|
+
emailErrors: null,
|
|
1336
|
+
recipientsErrors: null,
|
|
1337
|
+
succeededCount: 1,
|
|
1338
|
+
emailRecipientsCount: 1,
|
|
1339
|
+
hardBouncesCount: 0,
|
|
1340
|
+
spamComplaintsCount: 0,
|
|
1341
|
+
recipientFilter: EmailRecipientFilter.create({}),
|
|
1342
|
+
membersCount: 1,
|
|
1343
|
+
otherRecipientsCount: 0,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1184
1346
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { column } from '@simonbackx/simple-database';
|
|
2
|
-
import { EmailRecipient as EmailRecipientStruct, Recipient, Replacement } from '@stamhoofd/structures';
|
|
2
|
+
import { EmailRecipient as EmailRecipientStruct, Recipient, Replacement, TinyMember } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { QueryableModel } from '@stamhoofd/sql';
|
|
7
7
|
import { SimpleErrors } from '@simonbackx/simple-errors';
|
|
8
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
+
import { Member } from './Member';
|
|
8
10
|
|
|
9
11
|
export class EmailRecipient extends QueryableModel {
|
|
10
12
|
static table = 'email_recipients';
|
|
@@ -70,6 +72,12 @@ export class EmailRecipient extends QueryableModel {
|
|
|
70
72
|
@column({ type: 'string', nullable: true })
|
|
71
73
|
userId: string | null = null;
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Won't get send if it is a duplicate of another email recipient.
|
|
77
|
+
*/
|
|
78
|
+
@column({ type: 'string', nullable: true })
|
|
79
|
+
duplicateOfRecipientId: string | null = null;
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* Set when the email was send, but we received a hard bounce for this specific email
|
|
75
83
|
* Contains the full output we received in the bounce
|
|
@@ -134,11 +142,32 @@ export class EmailRecipient extends QueryableModel {
|
|
|
134
142
|
})
|
|
135
143
|
updatedAt: Date;
|
|
136
144
|
|
|
137
|
-
getStructure() {
|
|
145
|
+
async getStructure() {
|
|
146
|
+
return (await EmailRecipient.getStructures([this]))[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getStructureWithoutRelations() {
|
|
138
150
|
return EmailRecipientStruct.create(this);
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
static async getStructures(models: EmailRecipient[]) {
|
|
154
|
+
const memberIds = Formatter.uniqueArray(models.map(m => m.memberId).filter(m => m) as string[]);
|
|
155
|
+
const members = await Member.getByIDs(...memberIds);
|
|
156
|
+
return models.map((m) => {
|
|
157
|
+
const struct = EmailRecipientStruct.create(m);
|
|
158
|
+
|
|
159
|
+
if (m.memberId) {
|
|
160
|
+
const member = members.find(me => me.id === m.memberId);
|
|
161
|
+
if (member) {
|
|
162
|
+
struct.member = TinyMember.create(member);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return struct;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
141
170
|
getRecipient() {
|
|
142
|
-
return this.
|
|
171
|
+
return this.getStructureWithoutRelations().getRecipient();
|
|
143
172
|
}
|
|
144
173
|
}
|
package/src/models/Member.ts
CHANGED
|
@@ -315,7 +315,7 @@ export class Member extends QueryableModel {
|
|
|
315
315
|
/**
|
|
316
316
|
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
317
317
|
*/
|
|
318
|
-
static async
|
|
318
|
+
static async getMemberIdsForUser(user: User): Promise<string[]> {
|
|
319
319
|
const query = SQL
|
|
320
320
|
.select('id')
|
|
321
321
|
.from(Member.table)
|
|
@@ -332,14 +332,14 @@ export class Member extends QueryableModel {
|
|
|
332
332
|
);
|
|
333
333
|
|
|
334
334
|
const data = await query.fetch();
|
|
335
|
-
return data.map(r => r.members.id as string);
|
|
335
|
+
return Formatter.uniqueArray(data.map(r => r.members.id as string));
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
/**
|
|
339
339
|
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
340
340
|
*/
|
|
341
341
|
static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithRegistrations[]> {
|
|
342
|
-
return this.getBlobByIds(...(await this.
|
|
342
|
+
return this.getBlobByIds(...(await this.getMemberIdsForUser(user)));
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
static getRegistrationWithTinyMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithTinyMember {
|
|
@@ -13,6 +13,9 @@ export class RegistrationPeriod extends QueryableModel {
|
|
|
13
13
|
})
|
|
14
14
|
id!: string;
|
|
15
15
|
|
|
16
|
+
@column({ type: 'string', nullable: true })
|
|
17
|
+
customName: string | null = null;
|
|
18
|
+
|
|
16
19
|
@column({ type: 'string', nullable: true })
|
|
17
20
|
previousPeriodId: string | null = null;
|
|
18
21
|
|
package/src/models/User.ts
CHANGED
|
@@ -228,13 +228,19 @@ export class User extends QueryableModel {
|
|
|
228
228
|
await Database.update(query, [this.id, other.id]);
|
|
229
229
|
|
|
230
230
|
// Update payments
|
|
231
|
-
const query2 = 'UPDATE payments SET
|
|
231
|
+
const query2 = 'UPDATE payments SET payingUserId = ? WHERE payingUserId = ?';
|
|
232
232
|
await Database.update(query2, [this.id, other.id]);
|
|
233
233
|
|
|
234
234
|
// Update orders
|
|
235
235
|
const query3 = 'UPDATE webshop_orders SET userId = ? WHERE userId = ?';
|
|
236
236
|
await Database.update(query3, [this.id, other.id]);
|
|
237
237
|
|
|
238
|
+
// Update emails
|
|
239
|
+
await Database.update('UPDATE emails SET userId = ? WHERE userId = ?', [this.id, other.id]);
|
|
240
|
+
|
|
241
|
+
// Update email recipients
|
|
242
|
+
await Database.update('UPDATE email_recipients SET userId = ? WHERE userId = ?', [this.id, other.id]);
|
|
243
|
+
|
|
238
244
|
await other.delete();
|
|
239
245
|
}
|
|
240
246
|
|